如果一个多线程程序中每个线程处理的资源没有交集,没有依赖关系那么这是一个完美的处理状态。你不用去考虑临界区域(critical section),不用担心存在所谓的条件竞争(race condition),当然也不用去单行执行顺序,当然这种状态只是完美情况下,事实往往没有这么完美。
当多个线程进入临界区域对临界资源进行修改或者读取的时候,往往需要确定线程的执行顺序,以保证共享资源的可见性和相关操作的原子性。这就涉及到线程间的通信了,即
如果线程A正好进入临界区,他可能对临界资源进行修改或者读取,这时候他就要通知随时想要进入临界区域的线程B:“你丫的等一下,现在只准我来访问”。我们称这时候线程A拥有了访问临界区的锁。我们可以将锁看做是一个通行证,拥有锁的可以在临界区畅通无阻,而没有锁的则需要在门外等着锁。我们将多个线程的执行过程看做是接力赛,线程A拿着通行证玩遍临界区之后,还需要将通行证交给下一个想要进入临界区的线程。当然具体交给谁,你如果纯粹交给操作系统来决断,这就可能产生各种意想不到的后果。极有可能的是刚刚明明决定传给线程B的,但是就因为线程A多看了线程C一眼,从此就对上了眼,从而把通行证交给了C.....
扯得有点远,不过从上一段我们可以看出线程间最简单粗暴的通信可以通过加锁解锁来实现。最简单的方式就是synchronized同步块。如下程序所示:
1 private int count;2 3 public synchronized int increment() {4 return count++;5 }
这种说是通信方式,其实说是独占方式来的更准确些,其实使用synchronized同步块之后,能够访问进入临界区域的只有一个线程。
我们考虑另外一种情况,通过信号来实现线程间通信。就像古装剧里面,在进攻之前一般会发一些信号,一些等待的线程只有收到信号改变的时候才会运行,比如下面代码的这种情况:
1 public class SimpleSignal { 2 public static void main(String[] args) { 3 Signal signal = new Signal(); 4 SignalThread t1 = new SignalThread(signal); 5 SignalThread t2 = new SignalThread(signal); 6 t1.start(); 7 t2.start(); 8 try { 9 Thread.sleep(1000);10 } catch (InterruptedException e) {11 e.printStackTrace();12 }13 signal.start();14 }15 }16 17 class Signal {18 private boolean startAction = false;19 20 public synchronized void start() {21 this.startAction = true;22 }23 24 public synchronized boolean isStarted() {25 return this.startAction;26 }27 }28 29 class SignalThread extends Thread {30 private final Signal signal;31 32 public SignalThread(Signal signal) {33 this.signal = signal;34 }35 36 @Override37 public void run() {38 while (!signal.isStarted()) {39 // 什么也不做,等待可以开始行动40 }41 42 System.out.println("Thread:" + Thread.currentThread()43 + " Go Go Go!Fighting!");44 }45 46 }
上面的代码可以看出在主线程调用signal.start()之前,线程t1.t2都不会继续执行,而是阻塞在while循环中等待主线程给出的进攻信号。这中通信实现方式叫做忙等待(busy wait),线程t1和线程t2,一直在while循环判断条件是否符合,这时候会一直占用CPU处理时间,从CPU利用率上来说不是那么好。
那么又没有改进方法呢,当然是有的,不必像前面的一样傻傻的望着天空看是否有信号灯,假如事情顺利的话派探子前来告知。在等待的过程中完全可以放弃对CPU的占用,让CPU去处理其他更加紧急的事情,从而提高CPU的利用率。当有探子来报的时候,CPU则唤醒原来的线程继续执行。升级版本1.0代码如下:
1 public class SignalUpV1Test { 2 public static void main(String[] args) throws InterruptedException { 3 SignalUpV1 signal = new SignalUpV1(); 4 SignalThreadUpV1 t1 = new SignalThreadUpV1(signal); 5 SignalThreadUpV1 t2 = new SignalThreadUpV1(signal); 6 t1.start(); 7 t2.start(); 8 Thread.sleep(1000); 9 System.out10 .println("Now the Main Thread call doNotify of the signal Object!");11 signal.doNotify();12 }13 }14 15 class SignalThreadUpV1 extends Thread {16 private final SignalUpV1 signal;17 18 public SignalThreadUpV1(SignalUpV1 signal) {19 this.signal = signal;20 }21 22 @Override23 public void run() {24 try {25 // 这里线程等待,给CPU去执行其他事情,然后等着被唤醒26 signal.doWait();27 } catch (InterruptedException e) {28 e.printStackTrace();29 }30 System.out.println("Thread" + Thread.currentThread() + " Running");31 }32 33 }34 35 class SignalUpV1 {36 private final Object monitorObject = new Object();37 38 public void doWait() throws InterruptedException {39 // 注意在哪个对象上调用wait或者notify则必须对哪个对象加锁,而不能对其他对象加锁,否则会报IllegalMonitorStatus异常40 synchronized (monitorObject) {41 monitorObject.wait();42 }43 }44 45 public void doNotify() {46 synchronized (monitorObject) {47 monitorObject.notify();48 }49 }50 }51 52 输出为:53 Now the Main Thread call doNotify of the signal Object!54 ThreadThread[Thread-0,5,main] Running
可以看到线程t1或者t2必须等待主线调用监视对象的doNotify方法才会继续往下执行,否则会一直等待,当然从输出结果中也可以看出,doNotify一次只能唤醒一个线程,程序执行完后JVM还是没法退出因为有一个线程还是处于等待状态(要想都唤醒请使用notifyAll而不是notify)。同时还需要注意的一点是Object对象的wait和notify方法,必须在拥有该对象的锁之后才能调用,否则会报IllegalMonitorStatus异常。
这种通信方式还是会存在信号丢失的问题(Signal Missing)。即加入调用监视对象的doNotify方法在doWait方法之前,那么前面等待的线程可能永远无法被唤醒,解决这种问题的办法就是加一个标志位,来存储线程是否已经被唤醒过,在线程调用wait方法之前,判断线程是否已经被唤醒,如果没有则调用wait等待唤醒,如果有则不调用wait直接执行。升级版本2.0如下:
1 public class SignalUpV2Test { 2 public static void main(String[] args) { 3 SignalUpV2 signal = new SignalUpV2(); 4 SignalThreadUpV2 t1 = new SignalThreadUpV2(signal); 5 SignalThreadUpV2 t2 = new SignalThreadUpV2(signal); 6 7 // 假设先调用监视对象的doNotify方法 8 signal.doNotify(); 9 t1.start();10 t2.start();11 12 try {13 Thread.sleep(1000);14 } catch (InterruptedException e) {15 e.printStackTrace();16 }17 System.out18 .println("Now the main thread call the signal's doNotify method");19 signal.doNotify();20 }21 }22 23 class SignalThreadUpV2 extends Thread {24 private final SignalUpV2 signal;25 26 public SignalThreadUpV2(SignalUpV2 signal) {27 this.signal = signal;28 }29 30 @Override31 public void run() {32 try {33 signal.doWait();34 } catch (InterruptedException e) {35 e.printStackTrace();36 }37 System.out.println("Thread:" + Thread.currentThread() + " running!");38 }39 }40 41 class SignalUpV2 {42 /**43 * 是否已经被唤醒的标志位。防止先调用doNotify导致的信号丢失问题从而使线程一直等待被唤醒44 */45 private boolean isNotified = false;46 47 private final Object monitorObject = new Object();48 49 public void doWait() throws InterruptedException {50 synchronized (monitorObject) {51 if (!isNotified) {52 monitorObject.wait();53 }54 this.isNotified = false;55 }56 }57 58 public void doNotify() {59 synchronized (monitorObject) {60 this.isNotified = true;61 monitorObject.notify();62 }63 }64 }65 66 输出结果:67 Thread:Thread[Thread-1,5,main] running!68 Now the main thread call the signal's doNotify method69 Thread:Thread[Thread-0,5,main] running!
这个通信版本看起来天衣无缝,事实上在大多数情况下是。但是还有一个不幸的消息,就是操作系统可能无法抑制躁动的心灵。他可能会存在虚假唤醒的情况(Spurious Wakeups)。即存于等待状态的线程可能无缘无故的被唤醒,从而离开wait方法继续执行。解决这种问题的办法很简单,使用while循环判断代替if判断,这样即使线程被虚假唤醒还是会去校验唤醒状态标志位是否为true,如果标志位还是false,会继续进入wait状态。从而完美解决了这个问题。实际上这种使用while检测唤醒标识位的方式是通过自旋锁(Spin Lock)来实现的。自旋锁在处理的过程中不会进行备份然后完全离开线程运行状态,而是仍然会占用CPU的处理时间,但是不会有线程切换的开销。升级版本3.0的代码这里不给出了,只需把if改成while即可。