wait原理的讨论

Posted LuckyWangxs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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();

    wait原理的讨论

wait原理的讨论

javajava wait 原理 synchronized ReentrantLock 唤醒顺序

JAVA同步锁机制 wait() notify() notifyAll()

并发技术13条件阻塞Condition的应用

为什么 TCP 协议有 TIME_WAIT 状态