面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?

简介: 面试知识点:notify是随机唤醒线程吗(唤醒线程顺序)?

Java 开发的小伙伴,对 wait 方法和 notify 方法应该都比较熟悉,这两个方法在线程通讯中使用的频率非常高,但对于 notify 方法的唤醒顺序,有很多小伙伴的理解都是错误的,有很多人会认为 notify 是随机唤醒的,但它真的是随机唤醒的吗?

带着这个疑问,我们尝试休眠 100 个线程,再唤醒 100 个线程,并把线程休眠和唤醒的顺序保持到两个集合中,最后再打印一下这两个集合,看一下它们的执行顺序,如果它们的顺序是一致的,那说明 notify 是顺序唤醒的,否则则是随机唤醒的,notify 测试代码如下:

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
 
public class NotifyTest {
    //等待列表, 用来记录等待的顺序
    private static List<String> waitList = new LinkedList<>();
    //唤醒列表, 用来唤醒的顺序
    private static List<String> notifyList = new LinkedList<>();
 
    private static Object lock = new Object();
 
 
    public static void main(String[] args) throws InterruptedException{
 
        //创建50个线程
        for(int i=0;i<50;i++){
            String threadName = Integer.toString(i);
            new Thread(() -> {
                synchronized (lock) {
                    String cthreadName = Thread.currentThread().getName();
                    System.out.println("线程 ["+cthreadName+"] 正在等待.");
                    waitList.add(cthreadName);
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程 ["+cthreadName+"] 被唤醒了.");
                    notifyList.add(cthreadName);
                }
            },threadName).start();
 
        }
 
        for(int i=0;i<50;i++){
            synchronized (lock) {
                lock.notify();
            }
            TimeUnit.MILLISECONDS.sleep(10);
        }
        System.out.println("wait顺序:"+waitList.toString());
        System.out.println("唤醒顺序:"+notifyList.toString());
    }
}

执行结果如下:

wait顺序:[0, 2, 3, 1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 46, 48, 49]

唤醒顺序:[0, 2, 3, 1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 47, 46, 48, 49]

从上述打印的结果我们可以看出,使用 notify 并不是随机唤醒的,而是顺序唤醒的,虽然以上代码能证明这个结论,但为了更清楚的解释这个问题,我们查看了 notify 的实现源码,它的源码内容如下:

    /**
     * Wakes up a single thread that is waiting on this object's
     * monitor. If any threads are waiting on this object, one of them
     * is chosen to be awakened. The choice is arbitrary and occurs at
     * the discretion of the implementation. A thread waits on an object's
     * monitor by calling one of the {@code wait} methods.
     * <p>
     * The awakened thread will not be able to proceed until the current
     * thread relinquishes the lock on this object. The awakened thread will
     * compete in the usual manner with any other threads that might be
     * actively competing to synchronize on this object; for example, the
     * awakened thread enjoys no reliable privilege or disadvantage in being
     * the next thread to lock this object.
     * <p>
     * This method should only be called by a thread that is the owner
     * of this object's monitor. A thread becomes the owner of the
     * object's monitor in one of three ways:
     * <ul>
     * <li>By executing a synchronized instance method of that object.
     * <li>By executing the body of a {@code synchronized} statement
     *     that synchronizes on the object.
     * <li>For objects of type {@code Class,} by executing a
     *     synchronized static method of that class.
     * </ul>
     * <p>
     * Only one thread at a time can own an object's monitor.
     *
     * @throws  IllegalMonitorStateException  if the current thread is not
     *               the owner of this object's monitor.
     * @see        java.lang.Object#notifyAll()
     * @see        java.lang.Object#wait()
     */
    public final native void notify();

简单翻译一下上面的重点内容,notify 选择唤醒的线程是任意的,但具体的实现还要依赖于 JVM。也就是说 notify 的唤醒规则,最终取决于 JVM 厂商,不同的厂商的实现可能是不同的,比如阿里的 JVM 和 Oracle 的 JVM,关于 notify 的唤醒规则可能是不一样的。

那作为一个普通的程序员我们要研究的就是官方的 JVM 也就是 HotSpot 虚拟机,它的 notify 实现源码在 ObjectMonitor.cpp 中,具体源码如下:

DequeueWaiter 方法实现的源码如下:

从上述源码可以看出,在进行唤醒时,每次会从 _WaitSet 等待集合中获取第一个元素进行出队操作,这也说明了 notify 是顺序唤醒的。

总结

notify 唤醒线程的规则是随机唤醒还是顺序唤醒取决于 JVM 的具体实现,作为主流的 HotSpot 虚拟机中的 notify 的唤醒规则是顺序的,也就是 notify 会按照线程的休眠顺序,依次唤醒线程。


重量级锁(Monitor)的加锁和解锁流程?解锁是有顺序的吗?

重量级锁 MonitorObject 对象有 4 个属性,分别是:

  • _owner:当前锁的持有线程
  • _cxq:竞争栈
  • _entryList:一个队列
  • _waitSet:

在 Monitor 内部中,主要有四部分组成,分别是 owner、cxq、EntryList 和 waitSet。

1、其中 owner 表示当前所的持有者,记录是哪一个线程获取了当前锁;

2、cxq 是一个栈结构,EntryList 是一个队列结构,这两部分一起完成了当发生锁竞争时,记录线程的阻塞状态;

3、waitSet 是一个集合结构,当线程执行 wait 方法后会将当前线程存入到 waitSet 集合中进入等待状态,只有当执行 notify 或者 notifyAll 时才会唤醒 waitSet 中的相关线程。

从 waitSet中唤醒的线程并不会马上获取锁,而是会和其他线程一样进行锁的竞争操作。

_entryList和_cxq是锁的等待队列,_waitSet是调用了wait()方法的线程队列

加锁流程:

当线程 t1、t2、t3 一起获取一个重量级锁时,获取的时间顺序分别是 t1、t2 和 t3。

1、因为是线程 t1 首先到达,所以 t1 会获取成功, MonitorObject 的 _owner 会从 nullptr 变成 t1,线程 t1 的 markword 对象存储 MonitorObject 的地址引用并将最后两位标记为 10,表示重量级锁。此时线程 t2 和 t3 肯定获取锁失败。

2、线程 t2,t3 开获取失败后悔开始进行自旋操作【jdk1.6 以后固定自旋就弃用了】,首先预自旋 11 次,获取锁失败之后会自适应自旋,首先自旋 5000 次。在自旋期间若线程 t1 释放锁了,此时线程 t2 和 t3 会一起去抢占锁,若没有释放就会进入 _cxq 竞争栈中。这段时间还是抢占式的。

3、若第二步获取锁失败了,就会进入一个叫做 enterI 的方法,尝试获取 _owner ,失败之后会陷入自旋,较上次自旋次数少 200 次,若自旋期间获取成功就成功拿到锁了,这段时间还是抢占式的。

4、若第 3 步中还是获取失败了那么线程就会在 _cxq 中陷入阻塞状态了(park),直到 _owner 被释放才会被唤醒。从这里开始就是非抢占式的了。靠后竞争锁的线程会优先获取到锁。

从加锁解锁流程可以看出,线程会先进入 cxq ,当 owner 释放后才会将 cxq 中的唤醒进入 EntryList 队列,然后再获取锁。

其实这么做的主要目的是为了防止出现 ABA 问题

解锁流程:

当线程 t1 释放锁之后,就会将 _owner 设置成 nullptr。此时会根据 _cxq 和 _entryList 的状态做出不同的操作。

1、当_cxq 和 _entryList 都为空时直接返回,释放成功。

2、当 _cxq 不为空时,就会将 _cxq 中所有的节点移动到 _entryList 中,_cxq 按照后进先出的原则,之后进入 _cxq 的会先进入 _entryList。

3、当 _entryList 不为空时,使用 unpark 方法从队列头结点开始唤醒,然后返回。

所以说,根据上面的加锁流程,当 t1 释放锁之后,进入 _cxq 的顺序是先 t2 后 t3,所以离开 _cxq 进入 _entryList 的顺序是先 t3 后 t2。故在 t2 和 t3 中,t3 会先获得锁。

解锁是有序的验证

查看下列代码

 private static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println("t1 获取锁");
            synchronized (obj) {
                try {
                    System.in.read();
                    System.out.println("t1 释放");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        Thread.sleep(100);
        new Thread(() ->{
            synchronized (obj){
                System.out.println("t2 获取");
            }
        }).start();
        Thread.sleep(100);
        new Thread(() ->{
            synchronized (obj){
                System.out.println("t3 获取");
            }
        }).start();
        Thread.sleep(100);
        new Thread(() ->{
            synchronized (obj){
                System.out.println("t4 获取");
            }
        }).start();
    }

运行结果如下(运行多次结果都是一样的):从结果可以看出,当线程 t1 释放锁后,越靠后竞争锁的线程或优先抢占到锁。这就是上面加锁流程中第 4 步的体现。

t1 获取锁


t1 释放

t4 获取

t3 获取

t2 获取


Process finished with exit code 0


notify

  • notify()随机唤醒一个处在等待状态的线程  
  • notifyAll()唤醒所有处在等待状态的线程
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到") 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

唤醒和阻塞具体的使用

package thread.wait_notify;
 
public class waitDemo {
    private static class WaitTask implements Runnable{
        private Object lock;
        public WaitTask(Object lock){
            this.lock=lock;
        }
        @Override
        public void run() {
            synchronized (lock){
                System.out.println(Thread.currentThread().getName()+ "准备进入等待状态");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+ "等待结束,线程继续执行");
            }
        }
    }
    private static class Notify implements Runnable{
        private Object lock;
        public Notify(Object lock){
            this.lock=lock;
        }
        @Override
        public void run() {
            synchronized (lock){
                System.out.println("准备唤醒等待线程");
                //随机唤醒一个等待线程
                lock.notify();
                System.out.println("唤醒结束");
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
            Object lock=new Object();
            Thread t1=new Thread(new WaitTask(lock),"t1");
            Thread t2=new Thread(new WaitTask(lock),"t2");
            Thread t3=new Thread(new WaitTask(lock),"t3");
            Thread notify=new Thread(new Notify(lock),"notify");
            t1.start();
            t2.start();
            t3.start();
            Thread.sleep(100);
            notify.start();
    }
}

 

注意点

必须搭配synchroized使用,不然会直接报错

 

背后工作原理解析

  • 如果是调用notifyAll,就会将这三个线程都放入阻塞队列,然后进行竞争锁资源
  • 一定要明确锁的资源是谁,引起的竞争的必须是线程调用的锁的对象一定是要一样的,如果竞争的不是同一个锁,那么就不会进入同一个阻塞队列
  • 只有唤醒线程执行完毕,才会有阻塞队列线程的执行
  • 阻塞队列怎么理解,比如当前t1线程获得了锁资源,那么t2,t3如果想竞争这个锁,就得处于阻塞队列,当t1线程调用了wait方法,释放了锁资源,那么t2和t3就会去竞争锁资源,然后其中获得一个,依次类推,当三个线程都处于等待队列,当调用了notify线程,等待队列其中一个进入阻塞队列,但是阻塞队列就算只有一个线程,也不会立即得到锁,因为notify线程也会占用锁,必须等notify线程结束,释放锁

wait和sleep的区别

  • 其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间
  • 如果有共性就先介绍共性,如果没有,分别介绍即可
  • wait方法是Object类提供的方法,需要搭配synchroized锁来使用,调用wait方法会释放锁,等待线程会被其他线程唤醒或者超时自动唤醒,唤醒之后需要再次竞争synchronized锁才能继续执行
  • sleep是Thread类提供的方法(不一定要搭配synchronized使用),调用sleep方法进入TIMED_WAITING状态,如果占用锁也不会释放锁,时间到了自动唤醒

为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里

因为Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调

当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态(等待队列)直到其他线程调用这个对象上的 notify()方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁(在执行完锁的代码内容),以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

相关文章
|
10天前
|
存储 安全 Java
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
Java面试题:请解释Java内存模型(JMM)是什么,它如何保证线程安全?
52 13
|
10天前
|
监控 Java 调度
Java面试题:描述Java线程池的概念、用途及常见的线程池类型。介绍一下Java中的线程池有哪些优缺点
Java面试题:描述Java线程池的概念、用途及常见的线程池类型。介绍一下Java中的线程池有哪些优缺点
23 1
|
10天前
|
Java 调度
Java面试题:简述Java线程的生命周期及其状态转换。
Java面试题:简述Java线程的生命周期及其状态转换。
11 0
|
10天前
|
缓存 Java
Java面试题:描述Java中的线程池及其实现方式,详细说明其原理
Java面试题:描述Java中的线程池及其实现方式,详细说明其原理
14 0
|
10天前
|
设计模式 安全 Java
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
12 0
|
10天前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
15 0
|
10天前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
16 0
|
10天前
|
设计模式 安全 Java
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
12 0
|
10天前
|
设计模式 缓存 安全
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
9 0
|
10天前
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
16 0

相关实验场景

更多