wait原理的讨论

看到一篇关于SynchorinzedLock底层原理区别的文章,主要涉及两种锁的EntryListWaitSet的区别、waitnotify的区别(严格说是二者阻塞与唤醒的区别),前者从队尾唤醒,后者从队首唤醒,篇幅虽然长,但是讲解的非常清楚,仔细读,一定会有收获,学过的也会更加透彻,文章很宝贵,转载过来收藏(原文链接已在版权处标注,尊重原创,侵权删)~~

温馨提示:动图屏幕看不全的,可以点击打开,然后缩放浏览器

在这里插入图片描述

总结如下synchronized关键字的调用wait方法进入等到的线程和因为拿不到锁而等待线程是否同一种状态?blockingwaiting

别小看这个问题,要扯清这个问题需要大篇幅的文字,所以再次长文警告;而且笔者可以很负责任的告诉读者如果你能看懂这篇文章绝对会燃起你对并发编程学习的兴趣

1. 关于blocking状态的线程

synchronized关键字的blocking

在多线程编程的情况下,假设我们定义了一把锁,如果现在有10个线程来获取这把锁那么肯定只有第一个线程可以获取到锁,从而进入临界区(所谓临界区就是被锁保护起来的代码块);其他获取不到锁的线程都会被阻塞(关于阻塞你就可以理解为CPU放弃调度这个线程了),但是这些被阻塞的线程JVM是怎么处理的呢?先看一张图
在这里插入图片描述
上图t1获取锁,如果在t1没有释放的情况下其他线程也来获取锁,结果肯定是获取不到,从而进入阻塞状态,但是这些被阻塞的线程如果不存某种关系将来唤醒的时候就很麻烦(比如先唤醒谁呢?有人肯定会说那肯定先唤醒最先阻塞的那个线程啊,关键是JVM如何知道哪个线程最先阻塞的呢?)为了解决这个麻烦JVM设计了一个EntryList的双向链表的队列来维护这些阻塞的线程;如上图这样 t2tn被维护到了这个队列,当t1释放锁之后会去这个队列当中唤醒一个线程来获取锁,这里请读者们思考一些问题;到底是唤醒一个,还是全部唤醒呢?如果唤醒一个是随机唤醒还是顺序唤醒,如果是顺序唤醒是正序还是倒序呢?笔者直接给出答案,当t1释放锁的时候会从EntryList当中唤醒一个线程,而且顺序唤醒,而且倒序的,也就是先唤醒tn这个线程;但是值得注意的synchronized关键字是倒序唤醒,但是如果你使用ReentrantLock那么则是正序唤醒;那么这个结论如何证明了----笔者将会通过2个角度来证明

  1. 通过一个简单java应用来证明
  2. 是通过JDK内部关于ReentrantLock锁的实现来证明;(因为ReentrantLocksynchronized关键字都是实现同步锁,他们都有这么一个队列,原理差不多,其实就算我能通过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关键字相同,唯一不同的是当主线程释放锁之后去EntryListEntryList其实是C++的队列,ReentrantLock其实不存在EntryList这个队列,但是他有一个对象FairSync或者NonfairSync这个对象维护了一个队列类似EntryList,文中为了方便都称之为EntryList吧)当中唤醒线程的时候是正序的(先进先出从);由于ReentrantLock是用java语言实现的,可以通过查阅JDK源码来看看他的原理;synchronized需要查询JVMC++源码;如果大家对并发编程感兴趣可以给我留言,我会持续更新,看有么有机会来写一篇关于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所代表的nodet3所代表的node)
在这里插入图片描述
当封装好t7之后,这个时候t7所代表的node会进入到队列,进入队列之后如下图(介于图片大小问题中间的Node忽略了,比如t2所代表的nodet3所代表的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();

    }
}

在这里插入图片描述
现在随着你们项目的推进代码越来越多,jack肾再好也啪不动了,故而你们老板再请了一个同样经常看笔者博客的大神——女程序员roserose由于经常给我的博客点赞水平比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故而把所有线程叫醒了(jackrose),但是如果老板调用的是notify(只会叫醒一个),那么老板叫醒的是谁呢?假设叫醒的是jack还好说,因为本身只满足了女人的条件叫醒他不为过,但是如果叫醒一个——刚好叫醒的是rose,而rose发现条件不满足继续wait,此时jack也在wait这样就不合理了,明明老板找来了桥本有菜,可是因为叫醒的人不对从而导致没有人工作,这下老板不是亏大了?(据我所知桥本有菜很贵的);那么notify的叫醒规则是什么呢?关于这个问题笔者下次更新吧;其实这也是synchronized关键字wait的一个缺陷;在ReentrantLock当中这个问题得到了完美解决;ReentrantLock当中也提供了类似wait的功能叫做Condition,和wait不同是Condition可以有多个休息室,比如因为女人而waitjack可以进入A休息室,而因为加钱而waitrose可以进入B休息室(当然你也可以让rose也进入A休息室);这样当条件满足的时候可以根据条件叫醒不同休息室的人;synchronizedwait方法是让线程进入一个队列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就不合理啊,因为t2wait而阻塞的不能自动唤醒,需要notify唤醒;所以JVM需要再设计一个waitSet来存放因为条件不满足调用了wait而阻塞的线程;

那么所有存在waitSet当中的线程状态就称之为waiting状态;现在如果你搞清楚了waitingblocking那么接下来还有一个比较难的问题;所有waiting状态的线程必须经过blocking状态才能进入runing状态怎么理解?也就是一个waiting状态的线程是无法直接运行的,需要从waiting转换成blocking状态才能运行;说白了就是你调用notifyAll其实是把waitset当中所有阻塞线程转移到EntryList当中,然后当持有锁的线程释放锁之后再去EntryList当中的末尾(synchronized是末尾,ReentrantLock是最前面)唤醒一个执行;画幅图说明一下

现在持有锁的是main,假设t0-t9blockingjackrosewaiting则如下图所示
在这里插入图片描述
如果现在主线程调用notifyAll方法会唤醒jackrose,这里的唤醒其实是把它们转移到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之后
在这里插入图片描述
主线程在调用完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的原理一样

把上面那个jackrose加班的例子改一下,改成用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可以实现和synchronizedwait一样的功能,而且她支持多条件,这比wait更加丰富(也是一个经典面试题——ReentrantLocksynchronze关键字的区别,什么情况下使用),你可以从这个方面去回答的,当然他们的区别不止这些,有机会在讨论吧

关键他的源码是怎样呢?也就是调用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,操作更方便哦

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值