做 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 问题
相关视频:【Java必备知识】为锁正名-第9集-重量级锁阻塞队列为何分成cxq和EntryList_哔哩哔哩_bilibili
解锁流程:
当线程 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()方法时,它会释放这个对象的锁(在执行完锁的代码内容),以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。