本周主题:Java多线程技术
目录
一、Java中的多线程概念
1、线程的生命周期
2、线程的优先级
3、线程的几个主要概念
二、多线程的使用
1、线程创建方法
2、线程的优先级
3、线程的同步机制
三、Java多线程售票小实例
1、错误示范
2、正确示范
3、总结:
一、Java中的多线程概念
Java 为多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程是多任务的一种特别的形式,多线程使用了更小的资源开销。多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。
这里定义和线程相关的另一个术语 - 进程:也就是一个应用程序。一个进程包括由操作系统分配的内存空间,包含一个或多个线程(至少要有一个线程,叫做主线程)。一个线程不能独立的存在,它必须是进程的一部分。一个进程(程序)会一直运行,直到所有的非守护线程都结束运行后才能结束。
Java的线程是通过java.lang.Thread类来实现的。JVM启动时会有一个由主方法所定义的线程。可以通过创建Thread的实例来创建新的线程。每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
1、线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。在Java当中,线程通常都有五种状态:创建、就绪、运行、阻塞和死亡。
下图显示了一个线程完整的生命周期。
- 新建状态:使用new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:如果就绪状态的线程获取 CPU 资源,就可以执行run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 死亡状态:一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
2、线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。
Java 线程的优先级是一个整数,其取值范围是 1-10(Thread.MIN_PRIORITY - Thread.MAX_PRIORITY )。
默认情况下,每一个线程都会分配一个优先级 5(NORM_PRIORITY)。
具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。
3、线程的几个主要概念
在多线程编程时,你需要了解以下几个概念:
- 线程同步
- 线程间通信
- 线程死锁
- 线程控制:挂起、停止和恢复
二、多线程的使用
有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。
通过对多线程的使用,可以编写出非常高效的程序。不过请注意,如果你创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。
请记住,上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间!
1、线程创建方法
线程的创建有两种方式,第一种是继承Thread类,第二种是实现Runnable接口。
多线程原理:相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start()就是进入随时可以开始玩的状态!当CPU分配一个时间片段给你就是轮到你了,你就开始玩run(),当CPU的分配给你的时间片使用完毕,你(线程)就继续排队,等待下一次的run()。
调用start()后,线程会被放到等待队列,等待CPU调度,并不一定要马上开始执行,只是将这个线程置于可运行状态。然后通过JVM,线程Thread会调用run()方法,执行本线程的线程体。先调用start后调用run,这么麻烦,为了不直接调用run?就是为了实现多线程的优点,没这个start不行。
1、start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
2、run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。
注意:多线程就是分时利用CPU,在宏观上就是让所有线程一起执行 ,也叫并发。
- 采用实现 Runnable接口的方式创建多线程时,线程类只是实现了 Runnable 接口,还可以继承其他类。
- 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
- 1、写一个类继承自Thread类,重写run方法。用start方法启动线程:
/**
* 文件名:MyFirstThreadTest.java
* 功能描述:继承Thread类,重写run()方法实现多线程功能的测试Demo
*/
class MyThread extends Thread {
public MyThread() {
super();
}
public MyThread(String threadName) {
super(threadName);
}
@Override
public void run() {
super.run();
for(int i = 5;i > 0; i--) {
try {
Thread.sleep(100); //当前线程休眠100ms
}catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + (i));//输出线程名
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();
t2.start();
try {
for (int i = 1; i <= 5; i++) {
Thread.sleep(100); //当前线程(即main的主线程)休眠100ms
System.out.println(Thread.currentThread().getName()+"-->"+(i));
}
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
- 2、写一个类实现Runnable接口,实现run方法。用new Thread(Runnable target).start()方法来启动:
/**
* 文件名:RunnableTest.java
* 功能描述:使用Runnable接口实现Thread功能的测试Demo
*/
class RunThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.currentThread().sleep(100);//Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"-->"+i);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableTest {
public static void main(String[] args) {
Runnable rt1 = new RunThread();
Runnable rt2 = new RunThread();
Thread t1 = new Thread(rt1,"线程1");
Thread t2 = new Thread(rt2,"线程2");
t1.start();
t2.start();
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
2、线程的优先级
setPriority()和join()方法的使用举例:
/**
* 文件名:MyPriorityTest.java
* 功能描述:多线程优先级功能的测试Demo
*/
class MyThread extends Thread {
public MyThread() {
super();
}
public MyThread(String threadName) {
super(threadName);
}
@Override
public void run() {
super.run();
for (int i = 5; i > 0; i--) {
try {
Thread.sleep(100); //当前线程休眠100ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + (i));//输出线程名
}
}
}
public class MyPriorityTest {
public static void main(String[] args) {
Thread t1 = new MyThread("t1");
try {
t1.start();
//可以比较一下有无t1.join();语句的执行效果
t1.join(); //等待本线程执行完毕后,才会去执行其他线程,相当提升了t1对象的优先级为最高级(10)
for (int i = 1; i <= 5; i++) {
Thread.sleep(100); //当前线程(即main的主线程)休眠100ms
System.out.println(Thread.currentThread().getName() + "-->" + (i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
有无t1.join();语句的执行结果比较:
3、线程的同步机制
1)为什么要使用synchronized
在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。
2)实现原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
3)synchronized的三种应用方式
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法(实例方法),锁是当前实例对象 ,进入同步代码前要获得当前实例的锁
- 静态同步方法,锁是当前类的class对象 ,进入同步代码前要获得当前类对象的锁
- 同步方法块,锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
4)synchronized的作用
Synchronized是Java中解决并发问题的一种最常用最简单的方法 ,他可以确保线程互斥的访问同步代码
5、synchronized用法举例:
/**
* 文件名:MySynchronizedTest.java
* 功能描述:线程同步关键字synchronized的用法测试Demo
*/
public class MySynchronizedTest {
private int shareInt = 0;
class AddThread extends Thread {
@Override
public void run() {
super.run();
synchronized (MySynchronizedTest.this) { //对以下代码块加锁,代码块执行完毕,释放该锁
System.out.println("当前线程:"+Thread.currentThread().getName());//输出线程名
try {
System.out.println("自增之前,shareInt="+shareInt);
Thread.sleep(100); //当前线程休眠100ms
shareInt ++;
System.out.println("自增之后,shareInt="+shareInt);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class SubThread extends Thread {
@Override
public void run() {
super.run();
synchronized (MySynchronizedTest.this) { //对以下代码块加锁,代码块执行完毕,释放该锁
System.out.println("当前线程:"+Thread.currentThread().getName());//输出线程名
try {
System.out.println("自减之前,shareInt="+shareInt);
Thread.sleep(100); //当前线程休眠100ms
shareInt --;
System.out.println("自减之后,shareInt="+shareInt);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public MySynchronizedTest() {
Thread t1 = new AddThread();
Thread t2 = new SubThread();
t1.start();
t2.start();
/*
for (int i = 1; i <= 5; i++) {
Thread.sleep(100); //当前线程(即main的主线程)休眠100ms
System.out.println(Thread.currentThread().getName() + "-->" + (i));
}
*/
}
public static void main(String[] args) throws InterruptedException {
MySynchronizedTest test = new MySynchronizedTest();
}
}
使用synchronized进行加锁前后执行结果对比:
三、Java多线程售票小实例
1、错误示范
/**
* 文件名:SellThreadTest.java
* 功能描述:多线程售票测试Demo
*/
class SellThread implements Runnable {
private int i = 5; //票的数量
public void run() {
while (true) {
if (i > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->sell " + i--);
} else {
break;
}
}
}
}
/**
* 文件名:Test.java
* 功能描述:
*/
public class SellThreadTest {
public static void main(String[] args) {
SellThread sell = new SellThread();
Thread sell1 = new Thread(sell, "sellman1");
Thread sell2 = new Thread(sell, "sellman2");
Thread sell3 = new Thread(sell, "sellman3");
sell1.start();
sell2.start();
sell3.start();
}
}
没有同步控制变量,所以最后出现了一票多卖和-1张票的情况:
2、正确示范
class SellThread implements Runnable {
private int i = 5;
String key = " ";
public void run() {
while(true) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (key) {
if (i > 0) {
System.out.println(Thread.currentThread().getName() + "-->sell " + i--);
} else {
break;
}
}
}
}
}
public class SellThreadTest {
public static void main(String[] args) {
SellThread sell = new SellThread();
Thread sell1 = new Thread(sell, "sellman1");
Thread sell2 = new Thread(sell, "sellman2");
Thread sell3 = new Thread(sell, "sellman3");
sell1.start();
sell2.start();
sell3.start();
}
}
执行结果如下:
补充:如果用synchronized关键字修饰方法,则只能有一个线程获取访问这段方法的权限。也就是说,一个线程把票买完了才结束。
class SellThread implements Runnable {
private int i = 20;
String key = "";
public synchronized void run() {
while(true) {
if(i > 0) {
try {
Thread.sleep(100);
} catch(Exception e) {
}
System.out.println(Thread.currentThread().getName() + "-->sell " + i--);
}else {
break;
}
}
}
}
public class SellThreadTest {
public static void main(String[] args) {
SellThread sell = new SellThread();
Thread sell1 = new Thread(sell, "sellman1");
Thread sell2 = new Thread(sell, "sellman2");
Thread sell3 = new Thread(sell, "sellman3");
sell1.start();
sell2.start();
sell3.start();
}
}
执行结果:一个人把所有的票都卖完了!
3、总结:
1、synchronized()括号中需要的是一个对象,所以这里使用了String类的一个对象key,可以用this来代替key。
2、一个线程拿到synchronized括号中的对象以后,其他线程也是可以执行的,但是当它们需要用key的时候,发现key已经被别人拿走了,只能等着key被释放了。就像上厕所,坑被别人占了,只能等着了,但是等着的时候我还可以运行,比如玩玩手机什么的。
3、synchronized method(){} 可以防止多个线程同时访问这个对象的synchronized方法;如果一个对象A,有多个synchronized方法,只要一个线程访问了其中的一个,那么其他线程不能同时访问A的任何一个synchronized方法。但是可以访问对象B的synchronized方法。
4、synchronized static method(){} 可以防止多个线程同时访问这个类的synchronized方法;对类的所有对象均起作用,无论对象A、对象B、对象C的。