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


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

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

 

那不使用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技术等等。基础的东西是很重要,但是不能全身心的死磕基础技术,而放弃了更高层的应用。学习的优先级必然应该向使用方向来倾斜,而不应该将大部分精力花费到太多的用不到,甚至不需要理解的技术层面上。但这样又必然会造成知其然而不知其所以然的窘境。学习,是挺难的。

文章摘自:https://www.cnblogs.com/jilodream/p/22770136