整站快速排名优化,cpa推广平台,域名领域,网站的 营销渠道的建设做 Java 开发的小伙伴#xff0c;对 wait 方法和 notify 方法应该都比较熟悉#xff0c;这两个方法在线程通讯中使用的频率非常高#xff0c;但对于 notify 方法的唤醒顺序#xff0c;有很多小伙伴的理解都是错误的#xff0c;有很多人会认为 notify 是随机唤醒的#xf…做 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 ListString waitList new LinkedList();//唤醒列表, 用来唤醒的顺序private static ListString notifyList new LinkedList();private static Object lock new Object();public static void main(String[] args) throws InterruptedException{//创建50个线程for(int i0;i50;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 i0;i50;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 objects* 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 objects* 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 objects monitor. A thread becomes the owner of the* objects monitor in one of three ways:* ul* liBy executing a synchronized instance method of that object.* liBy executing the body of a {code synchronized} statement* that synchronizes on the object.* liFor 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 objects monitor.** throws IllegalMonitorStateException if the current thread is not* the owner of this objects 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、线程 t2t3 开获取失败后悔开始进行自旋操作【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.locklock;}Overridepublic 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.locklock;}Overridepublic void run() {synchronized (lock){System.out.println(准备唤醒等待线程);//随机唤醒一个等待线程lock.notify();System.out.println(唤醒结束);}}}public static void main(String[] args) throws InterruptedException {Object locknew Object();Thread t1new Thread(new WaitTask(lock),t1);Thread t2new Thread(new WaitTask(lock),t2);Thread t3new Thread(new WaitTask(lock),t3);Thread notifynew 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所有类的都继承了ObjectJava想让任何对象都可以作为锁并且 wait()notify()等方法用于等待对象的锁或者唤醒线程在 Java 的线程中并没有可供任何对象使用的锁所以任意对象调用方法一定定义在Object类中。
为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调
当一个线程需要调用对象的 wait()方法的时候这个线程必须拥有该对象的锁接着它就会释放这个对象锁并进入等待状态等待队列直到其他线程调用这个对象上的 notify()方法。同样的当一个线程需要调用对象的 notify()方法时它会释放这个对象的锁在执行完锁的代码内容以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁这样就只能通过同步来实现所以他们只能在同步方法或者同步块中被调用。