wait、notify、notifyAll你知道多少?

1.概述

​ 本篇博客将围绕synchronized同步锁的阻塞、等待、唤醒展开,探究wait、notify和notifyAll的实现原理。

2.monitor对象

在介绍上述的原理之前,先来了解一下什么是monitor对象。synchronized相关的阻塞、等待、唤醒操作离不开这个监视器对象。monitor在上一篇博客中也提到了,重量级的锁对象在对象头中的mark word中有一个指针指向了一个monitor对象。

monitor对象比较重要的四个属性如下图所示:

在这里插入图片描述

  • waitSet

    调用对象的wait()方法进入等待状态的线程会放在这个队列当中。

  • entryList

    因为sychronized关键字抢锁没有抢到的线程会出现在这个队列当中。

  • cxq

    也是一个阻塞队列,当waitSet集合当中的线程被唤醒,根据决策的不同 可以选择转移到cxq队列 或者是entryList队列。

  • owner

    当前持有锁对象的线程记录在owner属性。

3.先阻塞的线程先拿到锁还是后拿到锁?

当多个线程根据sychronized关键字去抢锁的时候,仅有1个线程会抢到锁,执行之后的逻辑,其余的线程都会进入entryList当中。那么问题来了?先阻塞的线程会在之后先拿到锁吗?

下面就编写代码来验证到底是怎样的顺序

public static void main(String[] args) throws InterruptedException {
    
    
    Object lock = new Object();
    log.debug(Thread.currentThread().getName() + "拿到锁");
    synchronized (lock){
    
    
        for (int i = 0; i < 5; i++) {
    
    
            new Thread(()->{
    
    
                log.debug(Thread.currentThread().getName() + "拿不到锁进入entryList");
                synchronized (lock){
    
    
                    log.debug(Thread.currentThread().getName() + "执行逻辑");
                }
            },"t" + (i+1)).start();
            TimeUnit.SECONDS.sleep(1);
        }
    }
    log.debug(Thread.currentThread().getName() + "释放锁");
}

代码比较简单,定义了一个lock锁,一开始主线程main 拿到了锁资源,进入同步块

执行for循环每间隔1秒启动一个线程去拿锁,这时锁是被main线程持有的,所以其他线程必定是拿不到锁的,会按顺序的进入到entryList当中。

5个线程都启动完毕之后,main线程结束,把锁释放掉。此时在阻塞队列当中的线程就可以去拿锁。

上面代码的输出的结果如下:

在这里插入图片描述

根据结果可以得出的结论是:后进入entryList的线程先拿到锁资源执行逻辑。

下面是entryList的示意图,5个线程进入entryList后,唤醒的顺序是从tail到head

在这里插入图片描述

ps:结果是基于hotspot VM 默认的启动参数而言的,本机采用的jdk的版本如下:在这里插入图片描述

4.先wait的线程先被唤醒还是后被唤醒?

通过第二部分的说明,可以知道的是调用锁对象的wait方法后,线程会进入等待状态,跑到waitSet集合当中等待唤醒。那么当有线程调用锁对象的notify之后,在waitSet中的线程被唤醒的顺序是怎样的呢?

下面是编写代码,来看看结果是怎样的。

public static void main(String[] args) throws InterruptedException {
    
    
        Object lock = new Object();
        for (int i = 0; i < 5; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                synchronized (lock) {
    
    
                    try {
    
    
                        log.debug(Thread.currentThread().getName() + "进入waitSet");
                        lock.wait();
                        TimeUnit.SECONDS.sleep(1);
                        log.debug(Thread.currentThread().getName() + "开始工作");
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }, "t" + (i + 1));
            thread.start();
            TimeUnit.SECONDS.sleep(1);
        }
        synchronized (lock){
    
    
         		for (int i = 0; i < 5; i++) {
    
    
                new Thread(()->{
    
    
                    log.debug(Thread.currentThread().getName() + "进入entryList");
                    synchronized (lock){
    
    
                        log.debug(Thread.currentThread().getName() + "开始工作");
                    }
                },"a" + (i + 6)).start();
                TimeUnit.SECONDS.sleep(1);
            }
            for (int i = 0; i < 5; i++) {
    
    
                log.debug("唤醒线程第" + (i+1) + "次");
                lock.notify();
                TimeUnit.SECONDS.sleep(1);
            }
            TimeUnit.SECONDS.sleep(1);
        }
        log.debug(Thread.currentThread().getName() + "释放锁");
    }

先解释一下代码,首先定义一个锁lock。第一个for循环每隔1秒创建启动一个线程,拿锁之后调用wait方法 会进入到waitset集合当中,同时把锁释放了,所以第一个for循环的执行结果就是t1-t5 5个线程依次的进入waitSet队列当中,如下图所示

在这里插入图片描述

之后main线程拿到锁,在同步块中的第一个for循环的意思是依次的创建启动5个线程,但是这5个线程需要抢锁才可以执行,这5个线程会依次的进入entryList,如下图所示

在这里插入图片描述

在同步块中的第二个for循环的意思是依次的调用了5次notify,唤醒在waitSet队列中的线程,唤醒的顺序是从head开始唤醒,唤醒的线程此时是拿不到锁的所以会进行队列转移。本机虚拟机的默认策略是如果cxq为空,那么被唤醒的线程会进入cxq队列,否则就进入entryList队列,如下图所示

在这里插入图片描述

之后main线程释放锁,唤醒阻塞队列的线程,此时cxq不为空,所以t1可以拿到锁执行代码,t1释放锁之后,唤醒entryList队列当中的线程,从tail开始唤醒,和第三节中的例子一样。

最后整个代码的输出结果如下:

在这里插入图片描述

如果还不足以说服,那么修改一下上面的测试代码,调整main线程中同步代码块中 2个for循环的顺序,如下:

public static void main(String[] args) throws InterruptedException {
    
    
        Object lock = new Object();
        for (int i = 0; i < 5; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                synchronized (lock) {
    
    
                    try {
    
    
                        log.debug(Thread.currentThread().getName() + "进入waitSet");
                        lock.wait();
                        TimeUnit.SECONDS.sleep(1);
                        log.debug(Thread.currentThread().getName() + "开始工作");
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }, "t" + (i + 1));
            thread.start();
            TimeUnit.SECONDS.sleep(1);
        }
        synchronized (lock){
    
    
            for (int i = 0; i < 5; i++) {
    
    
                log.debug("唤醒线程第" + (i+1) + "次");
                lock.notify();
                TimeUnit.SECONDS.sleep(1);
            }
            TimeUnit.SECONDS.sleep(1);
          	for (int i = 0; i < 5; i++) {
    
    
                new Thread(()->{
    
    
                    log.debug(Thread.currentThread().getName() + "进入entryList");
                    synchronized (lock){
    
    
                        log.debug(Thread.currentThread().getName() + "开始工作");
                    }
                },"a" + (i + 6)).start();
                TimeUnit.SECONDS.sleep(1);
            }
        }
        log.debug(Thread.currentThread().getName() + "释放锁");
    }

前面代码不变,在main线程的同步块中,第一个for循环改成唤醒在waitSet中的线程,根据我的理解,monitor中的结果如下图所示:

在这里插入图片描述

之后创建5个线程,拿不到锁会进入entryList,结果如下图所示

在这里插入图片描述

之后main线程释放锁,唤醒阻塞队列的线程,此时cxq不为空,所以t1可以拿到锁执行代码,t1释放锁之后,唤醒entryList队列当中的线程,从tail开始唤醒

控制台输出结果如下:

在这里插入图片描述

所以先wait的的线程是否先唤醒执行代码,要看cxq是否为空,如果cxq为空还要判断他在阻塞队列当中的位置。所以具体情况要具体的分析。

5. notifyAll的唤醒有顺序吗?

这一部分探究notifyAll的唤醒顺序,同样编写测试代码,如下:

public static void main(String[] args) throws InterruptedException {
    
    
    Object lock = new Object();
    for (int i = 0; i < 5; i++) {
    
    
        Thread thread = new Thread(() -> {
    
    
            synchronized (lock) {
    
    
                try {
    
    
                    log.debug(Thread.currentThread().getName() + "进入waitSet");
                    lock.wait();
                    TimeUnit.SECONDS.sleep(1);
                    log.debug(Thread.currentThread().getName() + "开始工作");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }, "t" + (i + 1));
        thread.start();
        TimeUnit.SECONDS.sleep(1);
    }
    synchronized (lock){
    
    
        lock.notifyAll();
        TimeUnit.SECONDS.sleep(1);
        for (int i = 0; i < 5; i++) {
    
    
            new Thread(()->{
    
    
                log.debug(Thread.currentThread().getName() + "进入entryList");
                synchronized (lock){
    
    
                    log.debug(Thread.currentThread().getName() + "开始工作");
                }
            },"a" + (i + 6)).start();
            TimeUnit.SECONDS.sleep(1);
        }
    }
    log.debug(Thread.currentThread().getName() + "释放锁");
}

和前面的例子差不多,只是这里不是一个个唤醒而是直接调用notifyAll唤醒全部

先来看看执行的结果

在这里插入图片描述

输出的顺序是a10-a6 t5-t1 所以可以猜到,这10个线程都是从entryList队列当中被唤醒的。

结合图画理解,调用notifyAll,唤醒所有在waitSet中的线程,此时main线程锁没有释放,5个线程从waitSet的队头开始尾插的方式进入到entryList,如下图所示:

在这里插入图片描述

之后for循环,创建启动5个线程,拿不到锁进入entryList队列,如下图所示

在这里插入图片描述

此时main线程释放锁,唤醒entryList当中的线程,从tail开始执行,也就是第三节中得到的结论。

如果调换同步块中 notifyAll和for循环的顺序,那么结果应该是t5-t1开始工作 然后是a10-a6开始工作,读者可以自己去试一试。

所以调用notifyAll,会进行队列转移,将waitSet中的线程依次的转移到entryList当中。

猜你喜欢

转载自blog.csdn.net/gongsenlin341/article/details/112345638