第四章 Java并发编程基础
线程简介
什么是线程?
现代操作系统在一个运行程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
为什么要使用多线程?
(1)更多的处理器核心:线程是大多数操作系统调度的基本单元,一个程序作为一个进程来运行,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上。
(2)更快的响应时间:将数据一致性不强的操作派发给其他线程处理(也可以使用消息队列),如生成订单快照、发送邮件等。这样做的好处是响应用户请求的线程能够尽可能快地处理完成。缩短了响应时间,提升了用户体验。
(3)更好的编程模型:Java为多线程编程提供了良好、考究并且一致的编程模型,使开发人员能够更加专注于问题的解决,即为所遇到的问题建立适合的模型,而不是绞尽脑汁地考虑如何将其多线程化。
线程优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当线程的时间片用完了就会发生线程调度,并等待着下次分配。在Java线程中,通过一个整型成员变量priority来控制优先级,范围是1~10,在线程构建的时候可以通过setPriority(int)方法来修改优先级,默认优先级是5。
注意:线程优先级不能作为程序正确性的依赖,因为操作系统可以完全不用理会Java线程对于优先级的设定。
线程的状态
Java线程在运行的生命周期中可能处于6种不同的状态,在给定的一个时刻,线程只能处于其中的一个状态。
状态名称 | 说明 |
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程在自身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,如下图
由上图看出,线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。
注意:Java将操作系统中的运行和就绪两个状态合并称为运行状态。阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为ava.concurrent包中Lock接口对于住在的实现均使用了LoackSupport类中的相关方法。
Daemon线程
Daemon线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持型工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。Daemon属性需要在启动线程之前设置,不能再启动线程之后设置。
Daemon线程被用作完成支持性工作,但是在Java虚拟机退出时Daemon线程中的finally块并不一定会执行,如下代码
public class Daemon { public static void main(String[] args) { Thread thread = new Thread(new DaemonRunner(), "DaemonRunner"); thread.setDaemon(true); thread.start(); } static class DaemonRunner implements Runnable{ @Override public void run(){ try { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } }finally { System.out.println("DaemonThread finally run"); } } } }
运行Daemon程序,可以看到没有任何输出。main线程(非Daemon线程)在启动了线程DaemonRunner之后随着main方法执行完毕而终止,而此时Java虚拟机中已经没有非Daemon线程,虚拟机需要退出。Java虚拟机中的所有Daemon线程都需要立即终止,因此DaemonRunner终止,但是DaemonRunner中的finally块并没有执行。
注意:在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或清理资源的逻辑。
启动和终止线程
启动线程
线程对象在初始化完成之后,调用start()方法就可以启动这个线程。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。启动一个线程前,最好为这个线程设置设置线程名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员提供一些提示,自定义的线程最好能够起个名字。
理解中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。线程通过检查自身是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即时该线程被中断过,在调用该线程对象的isInterrupted()时依旧会返回false.从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isIntereupted()方法将返回false.
public class Interrupted { public static void main(String[] args) throws Exception { //sleepThread不停的尝试睡眠 Thread sleepThread = new Thread(new SleepRunner(), "SleepRunner"); sleepThread.setDaemon(true); //busyThread不停的运行 Thread busyThread = new Thread(new BusyRunner(), "BusyRunner"); busyThread.setDaemon(true); sleepThread.start(); busyThread.start(); //休眠5秒,让sleepThread和busyThread充分运行 TimeUnit.SECONDS.sleep(5); sleepThread.interrupt(); busyThread.interrupt(); System.out.println("sleepThread.isInterrupted is " + sleepThread.isInterrupted()); System.out.println("busyThread.isInterrupted is " + busyThread.isInterrupted()); //防止sleepThread和busyThread立即退出 TimeUnit.SECONDS.sleep(2); } static class SleepRunner implements Runnable{ @Override public void run(){ while (true){ try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } static class BusyRunner implements Runnable{ @Override public void run(){ while (true){ } } } }
输出:
sleepThread.isInterrupted is false
busyThread.isInterrupted is true
从结果看出,抛出InterruptedException的线程SleepThread,其中断标识位被清除了,而一直忙碌运作的线程没有被清除。
安全地终止线程
之前提到的中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外,还可以利用一个booleam变量来控制是否需要停止任务并终止该线程。在下面例子中,创建了一个线程CountThread,它不断地进行变量累加,而主线程尝试对其进行中断操作和停止操作
public class Shutdown { public static void main(String[] args) throws Exception { Runner one = new Runner(); Thread countThread = new Thread(one, "countThread"); countThread.start(); //睡眠1秒,main线程对countThread进行中断,使countThread能够感知中断而结束 TimeUnit.SECONDS.sleep(1); countThread.interrupt(); Runner two = new Runner(); countThread = new Thread(two, "countThread"); countThread.start(); //睡眠1秒,main线程对two进行中断,使countThread能够感知on为false中断而结束 TimeUnit.SECONDS.sleep(1); two.cancel(); } private static class Runner implements Runnable{ private long i; private volatile boolean on = true; @Override public void run(){ while (on && !Thread.currentThread().isInterrupted()){ i++; } System.out.println("count i = " + i); } public void cancel(){ on = false; } } }
输出结果如下所示(输出内容可能不同):
count i = 543487324
count i = 540898082
示例在执行过程中,main线程通过中断操作和cancel()方法均可使countThread得以终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅。
线程间通信
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,这将带来巨大价值。
volatile和synchronized关键字
Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行),所以线程在执行过程中,一个线程看到变量并不一定是最新的。
关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对改变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
等待/通知机制
一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作,整个过程开始于一个线程,而最终执行又是另一个线程。前者是生产者,后者就是消费者,这种模式隔离了“做什么”和“怎么做”,在功能层面上实现了解耦。如下代码
while(value != desire){ Thread.sleep(1000); } dosomething();
在条件不满足时就睡眠1秒,这样做的目的是防止过快的“无效”尝试,这种方式看似能够解决实现所需的功能,但是却存在如下问题
(1)难以确保及时性:在睡眠时,基本不消耗处理器资源,但是如果睡的太久,就不能及时发现条件已经变了
(2)难以降低开销:如果降低睡眠的时间,比如1毫秒,这样消费者能更加迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成无端浪费
以上两个问题,看似矛盾难以调和,但是Java通过内置的等待/通知机制能够很好地解决这个矛盾并实现所需的功能。
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超累java.lang.Object上
方法名称 | 描述 |
notify() | 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的锁 |
notifyAll() | 通知所有等待爱该对象上的线程 |
wait() | 调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回 |
wait(long, int) | 对于超时时间更细粒度的控制,可以达到纳秒 |
等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify()/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
在下列代码中,创建了两个线程——WaitThread和NotifyThread,前者检查flag值是否为false,如果符合要求,进行后续操作,否则在lock上等待,后者在睡眠了一段时间后对lock进行通知
public class WaitNotify { static boolean flag = true; static Object lock = new Object(); public static void main(String[] args) throws Exception{ Thread waitThread = new Thread(new Wait(), "WaitThread"); waitThread.start(); TimeUnit.SECONDS.sleep(1); Thread notifyThread = new Thread(new Wait(), "NotifyThread"); notifyThread.start(); } static class Wait implements Runnable{ @Override public void run() { //加锁,拥有lock的Monitor synchronized (lock){ //条件不满足时,继续wait,同时释放了lock的锁 while (flag){ try { System.out.println(Thread.currentThread() + "flag is true.wait@ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.wait(); }catch (InterruptedException e){ } } //条件满足时,完成工作 System.out.println(Thread.currentThread() + "flag is false.wait@" + new SimpleDateFormat("HH:mm:ss").format(new Date())); } } } static class Nofity implements Runnable{ @Override public void run() { //加锁,拥有lock的Monitor synchronized (lock){ //获取lock的锁,然后进行通知,通知时不会释放lock的锁 //直到当前线程释放了lock后,waitThread才能从wait方法中返回 System.out.println(Thread.currentThread() + "hold lock. notify@ " + new SimpleDateFormat("HH:mm:ss").format(new Date())); lock.notifyAll(); flag = false; try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } //再次加锁 synchronized (lock){ System.out.println(Thread.currentThread() + "hold lock again. sleep@" + new SimpleDateFormat("HH:mm:ss").format(new Date())); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
输出如下:
Thread[WaitThread,5,main] flag is true.wait @ 22:23:03
Thread[NotifyThread,5,main] hold lock. notify @ 22:23:04
Thread[NotifyThread,5,main] hold lock again. sleep @ 22:23:09
Thread[WaitThread,5,main] flag is false.running @ 22:23:14
上述第3行和第4行输出的顺讯可能会互换,而上述例子主要说明了调用wait()、notify()以及notifyAll()时需要注意的细节,如下:
- 使用wait()、notify()以及notifyAll()时需要先对调用对象加锁
- 使用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列
- notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifyAll()的线程释放锁之后,等待线程才有机会从wait()返回
- notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED
- 从wait()方法返回的前提是获得了调用对象的锁
从上述细节中可以看到,等待/通知机制依托于同步机制,其目的就是确保等待线程从wait()方法返回时能够感知到通知线程对变量做出的修改。以下描述了上例过程
在上图中,WaitThread首先获取了对象的锁,然后调用对象的wait()方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()方法返回继续执行。