线程间通信

  如果一个多线程程序中每个线程处理的资源没有交集,没有依赖关系那么这是一个完美的处理状态。你不用去考虑临界区域(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     @Override
37     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.out
10                 .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     @Override
23     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.out
18                 .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     @Override
31     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 method
69 Thread:Thread[Thread-0,5,main] running!

  这个通信版本看起来天衣无缝,事实上在大多数情况下是。但是还有一个不幸的消息,就是操作系统可能无法抑制躁动的心灵。他可能会存在虚假唤醒的情况(Spurious Wakeups)。即存于等待状态的线程可能无缘无故的被唤醒,从而离开wait方法继续执行。解决这种问题的办法很简单,使用while循环判断代替if判断,这样即使线程被虚假唤醒还是会去校验唤醒状态标志位是否为true,如果标志位还是false,会继续进入wait状态。从而完美解决了这个问题。实际上这种使用while检测唤醒标识位的方式是通过自旋锁(Spin Lock)来实现的。自旋锁在处理的过程中不会进行备份然后完全离开线程运行状态,而是仍然会占用CPU的处理时间,但是不会有线程切换的开销。升级版本3.0的代码这里不给出了,只需把if改成while即可。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java技术

Java多线程编程-(7)-使用ReentrantReadWriteLock实现Lock并发

ReentrantLock具有完全互斥排他的效果,即同一时间只能有一个线程在执行ReentrantLock.lock()之后的任务。

601
来自专栏余林丰

3.从AbstractQueuedSynchronizer(AQS)说起(2)——共享模式的锁获取与释放

  在上节中解析了AbstractQueuedSynchronizer(AQS)中独占模式对同步状态获取和释放的实现过程。本节将会对共享模式的同步状态获取和释放...

1805
来自专栏desperate633

Java并发之“饥饿”和“公平锁”(Starvation and Fairness)java中发生线程饥饿的原因java中实现公平锁公平锁性能考虑

如果一个线程的cpu执行时间都被其他线程抢占了,导致得不到cpu执行,这种情况就叫做“饥饿”,这个线程就会出现饥饿致死的现象,因为永远无法得到cpu的执行。解决...

481
来自专栏Linyb极客之路

并发编程之各种锁的简介

一、公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优...

3546
来自专栏程序猿DD

死磕Java并发:J.U.C之读写锁:ReentrantReadWriteLock

重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读...

1055
来自专栏Java 源码分析

ReentrantLock 与 AQS 源码分析

ReentrantLock 与 AQS 源码分析 1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没...

3477
来自专栏老马说编程

(81) 并发同步协作工具 / 计算机程序的思维逻辑

查看历史文章,请点击上方链接关注公众号。 我们在67节和68节实现了线程的一些基本协作机制,那是利用基本的wait/notify实现的,我们提到,Java并发包...

1819
来自专栏编程

“J.U.C”:Semaphore

信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个“共享锁”。 Java并发提供了两种加锁模式:共享锁和独占锁。前面LZ介绍的Reent...

1646
来自专栏Spark学习技巧

锁机制-java面试

何为同步?JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和mo...

2906
来自专栏LanceToBigData

Java多线程(一)

多线程在面试中经常会被问到,所以也是非常重要的知识。 看到一篇写的很不错的博客:http://www.cnblogs.com/GarfieldEr007/p/5...

1798

扫码关注云+社区