看到一篇关于Synchorinzed
与Lock
底层原理区别的文章,主要涉及两种锁的EntryList
与WaitSet
的区别、wait
与notify
的区别(严格说是二者阻塞与唤醒的区别),前者从队尾唤醒,后者从队首唤醒,篇幅虽然长,但是讲解的非常清楚,仔细读,一定会有收获,学过的也会更加透彻,文章很宝贵,转载过来收藏(原文链接已在版权处标注,尊重原创,侵权删)~~
温馨提示:动图屏幕看不全的,可以点击打开,然后缩放浏览器
总结如下:synchronized
关键字的调用wait方法进入等到的线程和因为拿不到锁而等待线程是否同一种状态?blocking
?waiting
?
别小看这个问题,要扯清这个问题需要大篇幅的文字,所以再次长文警告;而且笔者可以很负责任的告诉读者如果你能看懂这篇文章绝对会燃起你对并发编程学习的兴趣
在多线程编程的情况下,假设我们定义了一把锁,如果现在有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
)
这次代码改一下,不用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两个技术点来说明
首先我们假设这样一个场景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();
}
}
现在随着你们项目的推进代码越来越多,jack
肾再好也啪不动了,故而你们老板再请了一个同样经常看笔者博客的大神——女程序员rose
;rose
由于经常给我的博客点赞水平比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; // 是否有女人
static boolean isMoney = 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();
new Thread(() -> {
synchronized (object){
while (!isMoney){
log.debug("没有加钱,先休息不干活");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("干活了----有钱能使鬼推");
}
}, "rose").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();
}
}
上面代码你们老板并没有加钱只是找了女人,但是调用了notifyAll
把所有因为wait
方法而阻塞的线程全部叫醒了,但是只有jack
会工作——因为满足了女人条件。但是没有满足加钱条件所以rose
还是不会工作会一直阻塞——JVM
不会退出;
当然老板如果把isMoney=true
那么rose
也会干活,并且干活完成之后JVM
也会正常结束,这个笔者就不在演示了;
这里读者可以思考一个问题,因为老板调用的是notifyAll
故而把所有线程叫醒了(jack
和rose
),但是如果老板调用的是notify
(只会叫醒一个),那么老板叫醒的是谁呢?假设叫醒的是jack
还好说,因为本身只满足了女人的条件叫醒他不为过,但是如果叫醒一个——刚好叫醒的是rose
,而rose
发现条件不满足继续wait
,此时jack
也在wait
这样就不合理了,明明老板找来了桥本有菜,可是因为叫醒的人不对从而导致没有人工作,这下老板不是亏大了?(据我所知桥本有菜很贵的);那么notify
的叫醒规则是什么呢?关于这个问题笔者下次更新吧;其实这也是synchronized
关键字wait
的一个缺陷;在ReentrantLock
当中这个问题得到了完美解决;ReentrantLock
当中也提供了类似wait
的功能叫做Condition
,和wait
不同是Condition
可以有多个休息室,比如因为女人而wait
的jack
可以进入A
休息室,而因为加钱而wait
的rose
可以进入B
休息室(当然你也可以让rose
也进入A休息室);这样当条件满足的时候可以根据条件叫醒不同休息室的人;synchronized
的wait
方法是让线程进入一个队列WaitSet
(休息室),不管你是因为女人还是因为加钱——jack和rose都进入WaitSet
队列(同一个休息室,因为synchronized
只有一个休息室);
因为拿不到锁而阻塞的线程(t0-t9
)会进入EntryList
阻塞;因为某个条件不满足被wait
之后的线程进入WaitSet
队列阻塞;那么为什么JVM
不让wait
的线程直接进入到EntryList
当中呢?而是还要设计出来一个WaitSet
的队列出来专门存放wait
的线程呢?
其实主要是因为如果都存放在EntryList
当中那么JVM
是无法区分这些线程是为什么而被阻塞的;而且将来唤醒的时候也无法确定该不该唤醒;试想这么一个场景,t0
持有锁,t1
是因为拿不到锁而阻塞进入EntryList
当中阻塞,t2
早先持有锁,因为某个条件不满足调用了wait
也进入到EntryList
当中阻塞(这里我们假设JVM
把所有阻塞的线程都放到EntryList
当中);那么当t0
释放锁之后讲道理他应该自动唤醒一个线程的,假设现在他唤醒的是t2
就不合理啊,因为t2
是wait
而阻塞的不能自动唤醒,需要notify
唤醒;所以JVM
需要再设计一个waitSet
来存放因为条件不满足调用了wait
而阻塞的线程;
那么所有存在waitSet
当中的线程状态就称之为waiting
状态;现在如果你搞清楚了waiting
和blocking
那么接下来还有一个比较难的问题;所有waiting
状态的线程必须经过blocking
状态才能进入runing
状态怎么理解?也就是一个waiting
状态的线程是无法直接运行的,需要从waiting
转换成blocking
状态才能运行;说白了就是你调用notifyAll
其实是把waitset
当中所有阻塞线程转移到EntryList
当中,然后当持有锁的线程释放锁之后再去EntryList
当中的末尾(synchronized
是末尾,ReentrantLock
是最前面)唤醒一个执行;画幅图说明一下
现在持有锁的是main,假设t0-t9
是blocking
,jack
和rose
是waiting
则如下图所示
如果现在主线程调用notifyAll
方法会唤醒jack
和rose
,这里的唤醒其实是把它们转移到EntryList
末尾,如下图所示
那么这个究竟是我吹牛逼还是真的如此呢?我通过一个例子和源码来证明
首先看一个简单的java
例子
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import org.omg.CORBA.TIMEOUT;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "s")
public class TestWait2 {
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();
TimeUnit.SECONDS.sleep(1);
log.debug("1s之后主线程获取锁");
log.debug("因为jack wait释放了锁,主线程能够获取到");
log.debug("-----------关键看打印顺序----------------");
synchronized (object) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (object) {
log.debug("那些默默无闻的程序员coding");
}
}, "t" + i).start();
TimeUnit.MILLISECONDS.sleep(1);
}
isWoman=true;
object.notifyAll();
}
}
}
解释一下代码,首先jack
获取到了锁,然后调用了wait
方法进入了waitSet
阻塞,跟着主线程获取了锁,主线程获取锁之后启动了5个线程(t0-t4)
;这5个线程也要获取锁,但是因为现在锁被主线程持有所以这五个线程肯定是获取不到锁的,继而进入EntryList
阻塞;启动完5个线程之后主线程满足条件isWoman=true;
从而唤醒jack
,如果jack
是即时唤醒执行的话,那么打印的顺序肯定是jack
先执行(诚然结果也是jack
先执行,但是其实这并不能说明notifyAll
是即时唤醒),然后是t4-t0
执行;但是笔者讲过notifyAll
其实是转移线程到EntryList
,也就是说当主线程调用完object.notifyAll()
;之后EntryList
当中应该存在6个线程 t0、t1、t2、t3、t4、jack
,如下图所示
调用完notifyAll之后
主线程在调用完notifyAll
之后就释放了锁,在这个时候JVM
会去EntryList
当中的末尾唤醒一个线程就是jack(
这个上面我们已经证明过了——倒序);所以打印的结果应该是jack–>t4–>-t3—>t2---->t1—>t0
;
但是这其实不能说明问题,因为诚如我上文说的notifyAll
如果是即时唤醒也是这个打印顺序;但是如果改下代码就能看出问题了,把唤醒jack
放到启动五个线程前面;
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import org.omg.CORBA.TIMEOUT;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "s")
public class TestWait2 {
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();
TimeUnit.SECONDS.sleep(1);
log.debug("1s之后主线程获取锁");
log.debug("因为jack wait释放了锁,主线程能够获取到");
log.debug("-----------关键看打印顺序----------------");
synchronized (object) {
------------------------------------- //注意顺序换了------------------------
isWoman=true;
object.notifyAll();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (object) {
log.debug("那些默默无闻的程序员coding");
}
}, "t" + i).start();
TimeUnit.MILLISECONDS.sleep(1);
}
}
}
}
那么情况就不同了如下图
那么执行顺序也就变了,jack
的执行应该在最后(synchronized
是倒序的);
这样应该足以证明问题了吧;假设你还不信,那么可以来翻阅JDK
源码来证明这个问题,当然咯翻阅JDK
源码主要是看ReentrantLock
的实现,而不是wait
的实现,wait
的源码需要去看JVM
源码C++
代码;我们这里翻阅JDK
源码只是来看看ReentrantLock
当中的Condition
是如何实现的,他几乎和wait
的原理一样
把上面那个jack
和rose
加班的例子改一下,改成用ReentrantLock
来实现(注意看代码)
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "s")
public class TestCondition1 {
static ReentrantLock lock = new ReentrantLock();//锁对象
static boolean isWoman = false; // 是否有女人
static boolean isMoney = false; // 是否加钱了
//因为钱不满足而进入阻塞的队列
static Condition conditionMoney = lock.newCondition();
//因为女人不满足而进入阻塞的队列
static Condition conditionWoman = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {//jack
lock.lock();
while (!isWoman){//判断是否有女人
log.debug("没有女人 我去等待老板安排 先休息,安排好之后叫醒我");
try {
//jack线程进入阻塞,但是释放了锁
//进入特定的队列---conditionWoman
conditionWoman.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("开始工作啪啪啪啪");
lock.unlock();
}, "jack").start();
new Thread(() -> {
lock.lock();
while (!isMoney){
log.debug("没有加钱,先休息不干活");
try {
// 进入特定的队列---conditionWoman
conditionMoney.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("干活了----有钱能使鬼推");
lock.unlock();
}, "rose").start();
TimeUnit.SECONDS.sleep(5);
new Thread(() -> {//jack
lock.lock();
isWoman=true;
log.debug("jack 桥本有菜来了,你可以啪啪啪了");
log.debug("因为是女人满足,从特定的队列当中(conditionWoman)唤醒");
conditionWoman.signalAll();
lock.unlock();
}, "boss").start();
}
}
这里定义了两个条件队列相当于定义了两个休息室,不同条件进入不同休息室,jack
进入conditionWoman
,而rose
进入conditionMoney
,老板最后只满足了桥本有菜故而只唤醒jack——conditionWoman.signalAll();
rose是感觉不到的——甚至都不会醒来,刚刚synchronized
关键字使用wait
,调用notifyAll
会把rose
也叫醒,虽然rose
最后还是去阻塞了(因为条件还是没有满足),但是终究是被叫醒了一次;使用ReentrantLock
就没有这个问题了
自此可以知道ReentrantLock
当中Condition
可以实现和synchronized
的wait
一样的功能,而且她支持多条件,这比wait更加丰富(也是一个经典面试题——ReentrantLock
和synchronze
关键字的区别,什么情况下使用),你可以从这个方面去回答的,当然他们的区别不止这些,有机会在讨论吧
关键他的源码是怎样呢?也就是调用conditionWoman.await();
是否如我所说会进入到一个waitSet
的队列?
如果你有印象前面我们分析过所有因为拿不到锁的对象会进入到一个队列当中(ReentantLock当中的 FairSync这个对象所维护的双向链表队列),但是这里调用conditionWoman.await();
则线程进入的是ConditionObject这个对象所维护的双向链表队列;这里足可证明waiting和blokcing两种不同状态的线程是维护在不同队列的(需要注意的是waitSet和EntryList是C++中的说法,由于ReentrantLock
是java语言实现的,故而EntryList
对应是ReentrantLock
当中公平锁或者非公平锁中的队列,WaitSet对应的是ConditionObject
这个双向链表队列);
再来看一下ReentrantLock的唤醒方法conditionWoman.signalAll();
结论:调用signalAll或者notifyAll方法都是把waiting状态的线程转化成为blocking状态——因为JDK内部是完成了一个队列的转移,而不是即时唤醒执行
当然最后的最后我们再次来看看ReentrantLock的Condition被唤醒之后的调用顺序是怎样的
package com.shadow.test;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "s")
public class TestConddition2 {
static ReentrantLock lock = new ReentrantLock();//锁对象
static boolean isWoman = false; // 是否有女人
static Condition conditoon = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {//jack
lock.lock();
while (!isWoman){//判断是否有女人
log.debug("没有女人 我去等待老板安排 先休息,安排好之后叫醒我");
try {
//jack线程进入阻塞,但是释放了锁
conditoon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("开始工作啪啪啪啪");
lock.unlock();
}, "jack").start();
TimeUnit.SECONDS.sleep(1);
log.debug("1s之后主线程获取锁");
log.debug("因为jack wait释放了锁,主线程能够获取到");
log.debug("-----------关键看打印顺序----------------");
lock.lock();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
lock.lock();
log.debug("那些默默无闻的程序员coding");
lock.unlock();
}, "t" + i).start();
TimeUnit.MILLISECONDS.sleep(1);
}
//注意顺序换了------------------------
isWoman=true;
conditoon.signalAll();
lock.unlock();
}
}
首先jack获取到了锁,然后调用了 conditoon.await();方法进入了waitSet(ConditionObject维护的双向链表)阻塞,然后主线程获取了锁,主线程获取锁之后启动了5个线程(t0-t4);这5个线程也要获取锁,但是因为现在锁被主线程持有所以这五个线程肯定是获取不到锁的,继而进入EntryList(非公平锁或者公平锁维护的双向链表队列)阻塞;启动完5个线程之后主线程满足条件isWoman=true;继而唤醒jack因为jack是在五个线程之后被唤醒的,也就是最后进入队列的 t0、t1、t2、t3、t4、jack,所以按照ReentrantLock先进先出的规则jack最后执行(和synchronized相反,前面我们已经证明过了synchronized是jack先执行)
代码地址(代码中用了lombok,自己装在idea)
链接:https://pan.baidu.com/s/1djfBWmqotbUWimZ1ixFvMQ
提取码:3v3u
复制这段内容后打开百度网盘手机App,操作更方便哦