wait原理的讨论
Posted LuckyWangxs
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了wait原理的讨论相关的知识,希望对你有一定的参考价值。
看到一篇关于Synchorinzed
与Lock
底层原理区别的文章,主要涉及两种锁的EntryList
与WaitSet
的区别、wait
与notify
的区别(严格说是二者阻塞与唤醒的区别),前者从队尾唤醒,后者从队首唤醒,篇幅虽然长,但是讲解的非常清楚,仔细读,一定会有收获,学过的也会更加透彻,文章很宝贵,转载过来收藏(原文链接已在版权处标注,尊重原创,侵权删)~~
温馨提示:动图屏幕看不全的,可以点击打开,然后缩放浏览器
正文开始
总结如下:synchronized
关键字的调用wait方法进入等到的线程和因为拿不到锁而等待线程是否同一种状态?blocking
?waiting
?
别小看这个问题,要扯清这个问题需要大篇幅的文字,所以再次长文警告;而且笔者可以很负责任的告诉读者如果你能看懂这篇文章绝对会燃起你对并发编程学习的兴趣
1. 关于blocking状态的线程
synchronized关键字的blocking
在多线程编程的情况下,假设我们定义了一把锁,如果现在有10个线程来获取这把锁那么肯定只有第一个线程可以获取到锁,从而进入临界区(所谓临界区就是被锁保护起来的代码块);其他获取不到锁的线程都会被阻塞(关于阻塞你就可以理解为CPU
放弃调度这个线程了),但是这些被阻塞的线程JVM
是怎么处理的呢?先看一张图
上图t1获取锁,如果在t1没有释放的情况下其他线程也来获取锁,结果肯定是获取不到,从而进入阻塞状态,但是这些被阻塞的线程如果不存某种关系将来唤醒的时候就很麻烦(比如先唤醒谁呢?有人肯定会说那肯定先唤醒最先阻塞的那个线程啊,关键是JVM
如何知道哪个线程最先阻塞的呢?)为了解决这个麻烦JVM
设计了一个EntryList
的双向链表的队列来维护这些阻塞的线程;如上图这样 t2
到tn
被维护到了这个队列,当t1
释放锁之后会去这个队列当中唤醒一个线程来获取锁,这里请读者们思考一些问题;到底是唤醒一个,还是全部唤醒呢?如果唤醒一个是随机唤醒还是顺序唤醒,如果是顺序唤醒是正序还是倒序呢?笔者直接给出答案,当t1
释放锁的时候会从EntryList
当中唤醒一个线程,而且顺序唤醒,而且倒序的,也就是先唤醒tn这个线程;但是值得注意的synchronized
关键字是倒序唤醒,但是如果你使用ReentrantLock
那么则是正序唤醒;那么这个结论如何证明了----笔者将会通过2个角度来证明
- 通过一个简单
java
应用来证明 - 是通过
JDK
内部关于ReentrantLock
锁的实现来证明;(因为ReentrantLock
和synchronized
关键字都是实现同步锁,他们都有这么一个队列,原理差不多,其实就算我能通过ReentrantLock
来证明这个双向列表的队列真实存在也不能说明synchronized
关键字也有这么一个队列啊,确实是这样,但是由于ReentrantLock
的这个队列是java语言实现的,比较容易看懂,毕竟是母语啊,所以先看懂java语言级别的实现——synchronized
关键字是没有java
级别源码可看的,他是通过C++
代码来实现的,先看懂java
的实现再来看C++
会轻松点)
首先来看一个简单的java
应用,代码如下(先仔细阅读以下代码,下文我会对代码做解释,博客里面所有代码放到文末链接,读者可以自己下载)
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "shadow")
public class TestSynchronized
static List<Thread> list = new ArrayList<>();
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException
for (int i = 0; i < 10; i++)
Thread t = new Thread(() ->
synchronized (lock)
log.debug("thread executed");
try
//这里的睡眠没有什么意义,仅仅为了控制台打印的时候有个间隔 视觉效果好
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
, "t" + i);//给每个线程去了一个名字 t1 t2 t3 ....
list.add(t);
log.debug("---启动顺序 调度顺序或者说获取锁的顺序讲道理是正序0--9----");
synchronized (lock)
for (Thread thread : list)
//这个打印主要是为了看到线程启动的顺序
log.debug("-启动顺序--正序0-9", thread.getName());
thread.start();// CPU 调度?
//这个睡眠相当重要,如果没有这个睡眠会有很大问题
//这里是因为线程的start仅仅是告诉CPU线程可以调度了,但是会不会立马调度是不确定的
//如果这里不睡眠 就有有这种情况出现
// 主线程执行t1.start--Cpu没有调度t1--继续执行主线程t2-start cpu调度t2--然后再调度t1
//虽然我们的启动顺序是正序的(t1--t2),但是调度顺序是错乱的 t2---t1
TimeUnit.MILLISECONDS.sleep(1);
log.debug("-------执行顺序--正序9-0");
代码非常简单主线程main
启动,然后实例化了10个线程对象t0-t9
;继而把这个10个线程添加到一个List
当中(注意这里仅仅是实例化了十(10)个线程,并没有启动,如果将来启动这10个线程他们的run
方法里面的代码也非常简单,就是获取lock
这把锁,然后打印一句话);添加到数组之后main
线程接着往下执行;mian
线程获取锁(这里一定能获取成功,因为那10个线程还没启动,锁处于自由状态,所以能被main
获取);获取到锁之后main
线程执行了一个for
循环从list
当中依次顺序获取到上面存入到list
当中的那10个线程(由于ArrayList
是有序的)故而取出的顺序肯定是有序的(t0-t9
);取出来之后依次调用star方法启动这些线程;但是这里需要注意的是虽然我们已经保证取出来的线程是顺序的(t0-t9
),而且我们也保证了这些线程的start
方法是顺序调用的,但是你依然没法保证这些线程的调度(也就是我们常说的执行)顺序;为了保证t0-t9
的调度顺序我这里在线程start
之后,让main
线程sleep
了1毫秒;这样就能保证t0-t9
线程的调度或者说执行顺序;至于为什么要保证他们的调度顺序?
来解释一下为什么需要保证这个调度顺序呢?
这里所有代码的意图就是顺序启动线程(顺序调度线程),这些线程启动之后会去拿锁(lock)
肯定拿不到,因为这个时候锁被主线程持有
主线程还在for循环没有释放锁,所以在for循环里面启动的线程都是拿不到锁的
那么这些那不到锁的线程就会阻塞
也就t0----t9阳塞之后他们被存到了一个队列当中
这个JVM的源码中可以证明,我后面给大家看源码,
总之你现在记住所有拿不到锁的线程会阻塞进入到Entrylist这个队列当中
然后主线程执行完for循环后会释放放锁
继而会去这个队列当中去唤醒一个个线程————随机唤醒还是顺序唤醒呢?
假设是顺序唤醒,是倒序还是正序唤醒呢?
需要证明这个问题,就要保证所有因为拿不到锁而进入到这个队列当中的线程
他们的顺序必须是有序的,这样后面从他们的执行结果才能分析;
假设你 进入到阻塞队列的时候都是随机的,那么后面唤醒线程执行的时候必然也是随机的
那么则无法证明唤醒是否具备有序性
为了保证进入到队列当中的线程调度是有序的,主线程睡眠很有必要
那么为什么主线程睡眠1下就能保证这些线程的顺序调度呢?这个问题读者可以思考一下后而我会重点分析
好了现在我们来看结果
从上图可以看出首先10个线程的启动顺序(由于主线程睡眠了1毫秒故而启动顺序其实等于调度顺序)是t0-t9
;因为在启动线程的时候主线程没有释放锁,所以t0-t9
都因为拿不到锁进入了队列(EntryList
),又因为t0-t9
的调度(启动)顺序保证了,所以进入队列的顺序也保证了(t0
先进入队列,t9
最后进入队列);但是在主线程释放锁的时候,唤醒线程的顺序是都倒序的,先唤醒t9
,最后唤醒t0
;这里的结果可以说明JVM
在从队列当中唤醒的时候是唤醒一个,而不是全部唤醒,因为如果是全部唤醒,那么这些线程的执行顺序肯定是乱的,只有唤醒一个,而且还是顺序唤醒才能保证执行顺序是具备规则的(t9-t0
),而且是倒序唤醒的;那么这队列存在哪里呢?在java语言里使用synchronized
关键字如果变成了一把重量锁(关于什么是重量锁下次分析),那么这个锁对象(本文当中的lock
对象——Object lock = new Object()
)会关联一个C++
对象——ObjectMonitor
对象;这个监视器对象当中记录了持有当前锁的线程,记录了锁被重入的次数,同时他还有一个属性EntryList
用来关联那些因为拿不到锁而被阻塞的线程;如下图所示(先不要关心WaitSet
)
ReentrantLock的Blocking
这次代码改一下,不用synchronized
关键来保护临界区,而是换成ReentrantLock
,代码如下
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "s")
public class TestReentrantLock
static List<Thread> list = new ArrayList<>();
//代码都没有变,只是把synchronized关键字变成了ReentrantLock
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException
for (int i = 0; i < 10; i++)
Thread t = new Thread(() ->
lock.lock();
log.debug("thread executed");
try
TimeUnit.MILLISECONDS.sleep(200);
catch (InterruptedException e)
e.printStackTrace();
lock.unlock();
, "t" + i);
list.add(t);
log.debug("---启动顺序 调度顺序或者说获取锁的顺序讲道理是正序0--9----");
lock.lock();
for (Thread thread : list)
log.debug("-启动顺序--正序0-9", thread.getName());
thread.start();// CPU 调度?
TimeUnit.MILLISECONDS.sleep(1);
log.debug("-------执行顺序--正序9-0");
lock.unlock();
从结果可以看到使用ReentrantLock
来保护临界区的时候效果几乎和synchronized
关键字相同,唯一不同的是当主线程释放锁之后去EntryList
(EntryList
其实是C++
的队列,ReentrantLock
其实不存在EntryList
这个队列,但是他有一个对象FairSync
或者NonfairSync
这个对象维护了一个队列类似EntryList
,文中为了方便都称之为EntryList
吧)当中唤醒线程的时候是正序的(先进先出从);由于ReentrantLock
是用java
语言实现的,可以通过查阅JDK
源码来看看他的原理;synchronized
需要查询JVM
的C++
源码;如果大家对并发编程感兴趣可以给我留言,我会持续更新,看有么有机会来写一篇关于synchronized
关键的底层C++
代码实现;好了现在我们来翻翻JDK
源码中对ReentrantLock
的源码实现吧(其实,笔者早先写过一篇关于ReentrantLock
的源码实现,而且你可以去b站search
关键字"子路 AQS
"有视频版的AQS
源码分析),这里不做特别细致的源码分析,只做简单的源码分析;
首先看一下Node
类设计,由于Node类是AbstractQueuedSynchronizer
的一个内部类,我没有截取到类名,只截取到父类的名字(JDK
源码中Node
主要用来封装线程,因为node
类里面有一个属性就是Thread
类型的,你可以理解一个node
对象就是一个线程)
主要关心三个属性
Node prev 双向列表用来指向上一个node(也就是上一个线程)
Node next 双向链表用来指向下一个node(也就是下一个线程)
Thread thread 当前node所封装的线程
如果单纯看这三个属性,可以理解Node
类主要是为了线程之间有关联而设计的,因为如果没有这个Node
那么单独一个Thread
是很难描述清楚线程之间的关系的;比如上面代码中t0-t9
他们的阻塞顺序靠一个Thread
是很难表示的;有了这个Node
就很好表示了,比如node0
对象当中的thread=t0,prev=null;next=node1;
而node1
当中的thread=t1,prev=node0,next=node2…
以此类推吧(实际当中Node
类有很多属性的,这里不做讨论);
再来看AQS
这个同步框架最核心的类的设计;同样我们只关心他的几个属性
Node head //队列当中的对头,也就是第一个阻塞的线程封装出来的node对象
Node tail //队列当中的对尾,也就是最后一个阻塞的线程封装出来的node对象
int state //锁的重入次数
如果我们使用ReentrantLock
来保护临界区当一个线程拿不到锁的时候,会把这个线程封装成为一个Node
对象;比如当t7
来获取锁的时候则持有锁的线程是main
,重入次数为1,对头为t0
,队尾为t6
;自己被封装成为一个node
对象(此时还没有进入队列,也就是还没有关联之前)如下图(介于图片大小问题中间的Node
忽略了,比如t2
所代表的node
,t3
所代表的node
)
当封装好t7
之后,这个时候t7所代表的node
会进入到队列,进入队列之后如下图(介于图片大小问题中间的Node
忽略了,比如t2
所代表的node
,t3
所代表的node
)
接下来通过idea
当中的debug
来说明一下上面的理论是否正确
上图的debug
过程对于新手来说比较晦涩,可以多看几遍,或者文章末尾拿到笔者的代码自己去调试;主要需要说明的队列当中的对头关联并不是t0
,而是一个thread=null
,这个读者可以忽略(其实我在另一篇博客里面解释过了),也是就结果和我上面讲的有一些偏差,但是不影响,因为要解释这个thread=null
代价比较大,读者可以把图换一下就一模一样了(实际情况如下图)
ReentrantLock
当中的这个AQS
双向链表队列相当于synchronized
关键字当中的那个EntryList
双向链表队列;只不过ReentrantLock
这个队列是先进先出,而EntryList
则相反是先进后出;这个上面已经通过例子证明了;
其实ReentrantLock
的先进先出可以通过源码来说明的;我们可以看看他的解锁方法也就是unlock
方法看他如何唤醒线程的就真相大白了;
好了说了这么多最后给大家总结一下
不管是synchronized还是使用ReentrantLock来做同步,并发情况下
所有拿不到锁的线程都会进入一个双向链表去阻塞
而进入这个队列当中阻塞的线程的状态就是blocking状态
至于什么是waiting状态呢?
同样我会通过synchronized和ReentrantLock两个技术点来说明
2. waiting状态
首先我们假设这样一个场景jack
是您们公司的一名程序员,他由于经常看笔者的博客;故而水平非常的高,经常能解决一些核心问题,所以逼格也很高;而各位读者就是程序员x
,水平比较低,几乎没有逼格;假设你们老板在周末休息时间打电话叫所有程序员来加班,公司钥匙只有一把(进入到公司的人会把门锁了),所以能进到公司一定需要这把钥匙;这个时候jack
来了,但是前面说过jack
逼格很高他加班必须要你们老板给他安排一个女人,他才会啪啪啪(当然这里的啪啪啪是指敲键盘),不然他就会去休息,而你们是没有逼格的,进到公司就写代码;(有么有一点感同身受啊),基于这个场景我们来编程
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "s")
public class TestWait1
static Object object = new Object();//锁对象
static boolean isWoman = false; // 是否有女人
public static void main(String[] args)
new Thread(() -> //jack
synchronized (object)
while (!isWoman)//判断是否有女人
log.debug("没有女人 我去等待老板安排 先休息,安排好之后叫醒我");
try
TimeUnit.SECONDS.sleep(1000000);
catch (InterruptedException e)
e.printStackTrace();
log.debug("开始工作啪啪啪啪");
, "jack").start();
for (int i = 0; i <5 ; i++)
new Thread(() ->
synchronized (object)
log.debug("那些默默无闻的程序员coding");
, "程序员"+i).start();
代码其实很简单,就是jack
先获取到锁,然后发觉没有女人,不能啪啪啪(再次强调这里的啪啪啪指的是敲键盘);然后他就去休息了;然后其他彩笔程序员(for i=5
)由于获取不到锁而不能工作无法启动;结果如下图
这样显然不合理,因为jack
的女人问题,搞得其他五个人无法工作,可能被fire
,这像极了我们平时,老板叫加班不敢不去,如果因为jack
去不成那基本要被人事约谈,所以不合理;不合理的地方在于jack
调用了sleep
去阻塞,sleep
阻塞的线程是无法释放锁的;假设有一种API能够让线程阻塞,同时又把锁释放了那就最好,jack
他牛逼他去休息等女人,不影响其他人受虐心甘情愿的加班。JDK
当中对于synchronized
关键提供了一个wait
方法可以实现上述功能
把代码修改一下,把sleep
改成wait
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "s")
public class TestWait1
static Object object = new Object();//锁对象
static boolean isWoman = false; // 是否有女人
public static void main(String[] args)
new Thread(() -> //jack
synchronized (object)
while (!isWoman)//判断是否有女人
log.debug("没有女人 我去等待老板安排 先休息,安排好之后叫醒我");
try
//jack线程进入阻塞,但是释放了锁
object.wait();
catch (InterruptedException e)
e.printStackTrace();
log.debug("开始工作啪啪啪啪");
, "jack").start();
for (int i = 0; i <5 ; i++)
new Thread(() ->
synchronized (object)
log.debug("那些默默无闻的程序员coding");
, "程序员"+i).start();
再次运行,其他五个人可以工作了,而jack
则在等待女人(JVM
没有退出,因为jack
线程阻塞了)
但是五个彩笔只能写CRUD
关键高并发的核心代码还是得jack来啊,如果他休息项目基本要黄;故而老板没有办法只能满足他——找个女人来,找个桥本有菜来给jack(这就是大神和你的区别吧可能);难么找来之后怎么唤醒jack呢?jdk
当中提供了notify/notifyall
来唤醒因为wait
方法而被阻塞的线程
把代码再次改一下,添加一个boos
线程,来满足jack
的条件让isWoman=true
,然后调用notifyAll
来唤醒jack
,叫醒之后jack
啪啪啪完之后全部线程结束,JVM
退出
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "s")
public class TestWait1
static Object object = new Object();//锁对象
static boolean isWoman = false; // 是否有女人
public static void main(String[] args) throws InterruptedException
new Thread(() -> //jack
synchronized (object)
while (!isWoman)//判断是否有女人
log.debug("没有女人 我去等待老板安排 先休息,安排好之后叫醒我");
try
//jack线程进入阻塞,但是释放了锁
object.wait();
catch (InterruptedException e)
e.printStackTrace();
log.debug("开始工作啪啪啪啪");
, "jack").start();
for (int i = 0; i <5 ; i++)
new Thread(() ->
synchronized (object)
log.debug("那些默默无闻的程序员coding");
, "程序员"+i).start();
//这里睡眠主要是为了视觉效果,没什么意义
//5s之后叫醒jack
TimeUnit.SECONDS.sleep(5);
new Thread(() -> //jack
synchronized (object)
isWoman=true;
log.debug("jack 桥本有菜来了,你可以啪啪啪了");
object.notifyAll();
, "boss").start();
wait原理的讨论
javajava wait 原理 synchronized ReentrantLock 唤醒顺序