线程间通信

简介:   如果一个多线程程序中每个线程处理的资源没有交集,没有依赖关系那么这是一个完美的处理状态。你不用去考虑临界区域(critical section),不用担心存在所谓的条件竞争(race condition),当然也不用去单行执行顺序,当然这种状态只是完美情况下,事实往往没有这么完美。

  如果一个多线程程序中每个线程处理的资源没有交集,没有依赖关系那么这是一个完美的处理状态。你不用去考虑临界区域(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 同步块

     这种说是通信方式,其实说是独占方式来的更准确些,其实使用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 }
SimpleSignal

  上面的代码可以看出在主线程调用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
SignalUpV1

  可以看到线程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!
SignalUpV2

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

 

黎明前最黑暗,成功前最绝望!
相关文章
|
2月前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
42 1
[Java]线程生命周期与线程通信
|
5月前
|
Java
实现Java多线程中的线程间通信
实现Java多线程中的线程间通信
|
1月前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
39 3
|
6月前
|
Java 程序员
从菜鸟到大神:JAVA多线程通信的wait()、notify()、notifyAll()之旅
【6月更文挑战第21天】Java多线程核心在于wait(), notify(), notifyAll(),它们用于线程间通信与同步,确保数据一致性。wait()让线程释放锁并等待,notify()唤醒一个等待线程,notifyAll()唤醒所有线程。这些方法在解决生产者-消费者问题等场景中扮演关键角色,是程序员从新手到专家进阶的必经之路。通过学习和实践,每个程序员都能在多线程编程的挑战中成长。
61 6
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
25 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
53 1
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
41 1
|
2月前
|
Java
|
6月前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
114 0
|
2月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
129 0