java 多线程开发系列之七:玩转多线程(线程的协作)

线程的协作多种多样,这次主要说说wait和notify两种Object的方法。这也是java早期原生的重要的协作机制之一。
先说下这两个英文单词:
wait [weɪt] v.等待;等候;(尤指长期地)希望,盼望,期待
notify [ˈnəʊtɪfaɪ] vt.通知;(正式)通报;
也就是一个等待,一个通知。
一般的使用场景就是:
线程a,运行到某一刻发现条件不满足,执行等待方法,
线程b,将条件满足,通知等待的线程,
线程a,收到通知请求,判断等待条件是否满足,满足后继续执行,不满足继续等待。

核心思路就是下边这图这样

waitnotify

 

那不使用wait/notify等待通知的方式来协作,线程a就一直while循环,等待条件满足,再继续执行可以么?
当然可以,这种情况我们也称之为 忙等(busy-waiting / spin)。自旋锁采用的就是这个逻辑。
但是自旋往往比较占用性能,大部分场景没这个必要。
我们更多的时候还是希望线程主动让出占用的cpu资源,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )等到条件合适再继续占用。

在确定完使用场景,我们还要注意到,wait 和notify 的使用,是放置在Object 方法上的,为了保证不被乱调,只有拿到了该Object的监视器锁的线程才有资格调用wait/notify方法的,否则就会报出IllegalMonitorStateException的运行时异常。注意看注释的解释:当前线程如果不是object 的显示器锁的持有者,就抛出该异常。
IllegalMonitorStateException if the current thread is not the owner of the object's monitor。
这里所谓的监视器锁,就是我们平常说的同步锁,synchronized锁。
同理由于同步块的存在,被通知唤醒的线程,只是进入就绪状态,还是需要再次拿到锁才可以继续进行。
具体的几个核心方法源码如下:

 1     public final native void notify();
 2 
 3     public final native void notifyAll();
 4 
 5     public final void wait() throws InterruptedException {
 6         wait(0L);
 7     }
 8 
 9     public final native void wait(long timeoutMillis) throws InterruptedException;
10 
11     public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
12         if (timeoutMillis < 0) {
13             throw new IllegalArgumentException("timeoutMillis value is negative");
14         }
15 
16         if (nanos < 0 || nanos > 999999) {
17             throw new IllegalArgumentException(
18                                 "nanosecond timeout value out of range");
19         }
20 
21         if (nanos > 0 && timeoutMillis < Long.MAX_VALUE) {
22             timeoutMillis++;
23         }
24 
25         wait(timeoutMillis);
26     }

都是包装了一下,然后最终调用本地方法,其中

wait() 线程进入等待(更准确的说法是进入了lock对象的等待集),(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )并释放锁
wait(long t) 线程进入等待,等待t的时间,同时释放锁,如果等待时间超过t,则结束等待,进入就绪状态,重新开始争夺监视器锁
notify() 随机通知(唤醒)一个等待中的线程结束等待
notifyAll() 通知(唤醒)所有等待中的线程结束等待

了解了这些必要条件,我们来看一个经典的生产消费者通知的问题:

 1 /**
 2  * @discription
 3  */
 4 public class WNStudy {
 5 
 6     public static void main(String[] args) {
 7         Cache cache = new Cache();
 8         Runnable producer = () -> {
 9             try {
10                 for (int i = 0; i < 5; i++) {
11                     String id = "id" + i;
12                     cache.put(id);
13                 }
14             } catch (InterruptedException e) {
15                 //dosth
16             }
17         };
18 
19 
20         Runnable consumer = () -> {
21             try {
22                 for (int i = 0; i < 5; i++) {
23                    cache.take();
24                 }
25             } catch (InterruptedException e) {
26                 //dosth
27             }
28         };
29         Thread tp1 = new Thread(producer);
30         Thread tp2 = new Thread(producer);
31         Thread tc1 = new Thread(consumer);
32         Thread tc2 = new Thread(consumer);
33 
34         tp1.start();
35         tp2.start();
36         tc1.start();
37         tc2.start();
38     }
39 
40     static class Cache {
41         private final Object lock = new Object();
42         private static final int MAX_LEN = 3;
43         private String[] idCache = new String[MAX_LEN];
44 
45         private int size = 0;
46 
47 
48         private void put(String id) throws InterruptedException {
49             synchronized (lock) {
50                 while (size == (MAX_LEN)) {
51                     lock.wait();
52                 }
53                 idCache[size] = id;
54                 size++;
55                 System.out.println("producer ok +" + id);
56                 lock.notifyAll();
57             }
58         }
59 
60         private String take() throws InterruptedException {
61             synchronized (lock) {
62                 while (size == 0) {
63                     lock.wait();
64                 }
65                 String id = idCache[size - 1];
66                 idCache[size - 1] = null;
67                 size--;
68                 lock.notifyAll();
69                 System.out.println("consumer ok -" + id);
70                 return id;
71             }
72         }
73     }
74 }

核心逻辑是 tp1/tp2 两个生产者不断的向栈中推数据

tc1/tc2 两个消费者不断的从栈中取数据

执行后结果如下:

 1 producer ok +id0
 2 consumer ok -id0
 3 producer ok +id1
 4 consumer ok -id1
 5 producer ok +id0
 6 consumer ok -id0
 7 producer ok +id1
 8 producer ok +id2
 9 consumer ok -id2
10 consumer ok -id1
11 producer ok +id3
12 producer ok +id2
13 consumer ok -id2
14 consumer ok -id3
15 producer ok +id3
16 producer ok +id4
17 consumer ok -id4
18 consumer ok -id3
19 producer ok +id4
20 consumer ok -id4

我们可以看到栈的数据是符合入栈出栈的逻辑的。

回到api本身,wait(long t),表示时间到了,线程会自动被唤醒进入就绪状态,无需其它线程唤醒。
同时需要注意的是,唤醒通知非累加动作,notify只影响到等待集中的线程,未进入等待集的线程,不受唤醒的影响,也不会累加这个状态,所收到的唤醒通知也会被丢弃,需要进入wait时重新被唤醒。

不知道大家发现没有,如果由于notify 是随机的,而notifyAll 唤醒所有线程后,究竟是哪个线程抢到锁继续执行,也是未知的。(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )因此可能不是期待中的线程唤醒。比如例子中,可能队列满了,唤醒抢到锁的线程仍然是消费者线程。从而导致无意义的唤醒和重新等待,从而浪费了性能。这种现象我们称之为惊群效应。传统的wait/notify 方式是无法避免这种情况的,需要由更高级的ReentrantLock 配合condition才可以。这个放在后边的文章来说吧。

由于随机性,和不确定性,因此当线程被唤醒后,往往需要重新检查就绪条件,如果不满足还需要重新等待,从而避免被其它线程虚假唤醒,

反思

今天看了下,这个主题中,距离上一篇相关文章已经有十年了,想想很惭愧,但是再一细想,自己也一直没有松懈。
反思了一下为什么拖了这么久,
一方面是使用的技术不断的在演进,刚开始的项目中,接触到的业务中多线程还需要使用wait notify来协作,后边主要在做服务端开发,服务端又慢慢的进入到分布式场景中,这种进程内的锁的使用就更少了。即使有用到,完整的并发工具包也早已满足大部分的业务需要。
一方面是操作的业务不断的在变化,需要学习的东西也不断的在变,最早是原生自研的框架,后边是spring,springboot ,再然后是容器化技术,消息队列,读写分离,db/缓存技术,ai技术等等。基础的东西是很重要,但是不能全身心的死磕基础技术,而放弃了更高层的应用。学习的优先级必然应该向使用方向来倾斜,而不应该将大部分精力花费到太多的用不到,甚至不需要理解的技术层面上。但这样又必然会造成知其然而不知其所以然的窘境。学习,是挺难的。

posted @ 2026-08-31 10:34  王若伊_恩赐解脱  阅读(138)  评论(0)    收藏  举报