建设部网站八大员查询广西seo经理
目录
〇、先总结一下这三个方法带来的Java线程状态变化
一、obj.wait()
1.1 作用
1.2 使用前需要持有线程共享对象的锁
1.3 使用技巧
二、obj.notify(All)()
1.1 notify() 方法
1.1.1 调用notify()或notifyAll()不会释放线程的锁
1.2 notifyAll() 方法
1.3 使用技巧
三、使用实例
四、wait()/notify()/notifyAll() 为什么定义在 Object 类中?
wait()、notify/notifyAll() 方法都是Object的本地final方法,无法被重写。
〇、先总结一下这三个方法带来的Java线程状态变化
当Java线程调用wait()方法后,该线程会进入等待队列,并且会释放占用的锁资源。线程状态会变为WAITING或TIMED_WAITING。该线程不会被挂起到外存,而是在内存中等待被唤醒。线程等待的条件通常是由其他线程调用notify()或notifyAll()方法来唤醒该线程。
当线程被唤醒时,它会重新尝试获取锁资源并从wait()方法返回。线程状态会变为BLOCKED,直到它获得了锁资源为止。如果成功获取锁资源,线程状态会变为RUNNABLE,然后可以继续执行。如果获取锁资源失败,则线程会继续等待,并且状态会维持在BLOCKED或WAITING或TIMED_WAITING状态,直到它再次被唤醒。
需要注意的是,线程在等待期间会消耗一定的资源,因此应该避免过多的线程等待。另外,线程在等待期间不会占用CPU时间片,因此可以减少CPU的利用率,提高系统的性能。
一、obj.wait()
1.1 作用
wait()是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用wait()方法。wait方法还有可以传入等待时长的,可以让线程等待指定的时间后自动被唤醒。调用wait()会使Java线程进入到WAITING状态,调用wait(long time)会使Java线程进入到TIMED_WAITING状态。(WAITING和TIMED_WAITING状态就是阻塞状态)
当一个线程调用一个共享变量的wait()方法时,该线程会阻塞(等待)。直到发生以下几种情况才会恢复执行:
- 其他线程调用了该共享对象的 notify() 方法或者 notifyAll() 方法(继续往下走)
- 其他线程调用了该线程的 interrupt() 方法,该线程会 InterruptedException 异常返回
等待线程:假设调用的是obj对象的wait()方法,wait的执行线程,也就是被暂停的线程,就称为对象obj上的等待线程。对象的wait方法可能被不同的线程执行,所以同一个对象可能会有多个等待线程。
1.2 使用前需要持有线程共享对象的锁
在使用wait()、notify()和notifyAll()方法方法前,需要先持有锁。如果调用线程共享对象的wait()、notify()和notifyAll()方法的线程没有事先获取该对象的监视器锁,调用线程会抛出IllegalMonitorStateException 异常。当线程调用wait() 之后,就会释放该对象的监视器锁。
使用wait()、notify()和notifyAll()方法方法前,需要先持有锁:
- 表象:wait、notify(ALL)方法需要调用 monitor 对象
- 本质:Java的线程通信实质上是共享内存,而不是直接通信
那么,一个线程如何才能获取一个共享变量的监视器锁?
1、执行synchronized 同步代码块,使用该共享变量作为参数。
synchronized(共享变量) {// TODO
}
2、调用该共享变量的同步方法(synchronized 修饰)
synchronized void sum(int a, int b) {// TODO
}
如下代码示例,线程A与线程B,在线程A中调用共享变量obj的wait()方法,在线程B中进行唤醒notify()。
/*** Object的Wati()方法的使用*/
@Slf4j
public class WaitTest {public static void main(String[] args) {// 定义一个共享变量Object obj = new Object();// 创建线程AThread threadA = new Thread(new Runnable() {@Overridepublic void run() {log.info("线程" + Thread.currentThread().getName()+"开始执行");try {// 获取共享变量的对象锁synchronized(obj){// 线程A 等待log.info("线程" + Thread.currentThread().getName()+"等待");// 调用wait(),线程A阻塞,并且释放掉获取到的obj的对象锁obj.wait();}} catch (InterruptedException e) {e.printStackTrace();}log.info("线程" + Thread.currentThread().getName()+"执行结束");}},"A");// 创建线程BThread threadB = new Thread(new Runnable() {@Overridepublic void run() {log.info("线程" + Thread.currentThread().getName()+"开始执行");// 获取共享变量锁synchronized (obj){// 线程B 唤醒或者中断 调用obj的唤醒操作或者使A线程中断的操作都可以将正在阻塞的A线程唤醒log.info("线程" + Thread.currentThread().getName()+"唤醒");obj.notify(); // 唤醒操作// threadA.interrupted(); // 中断操作}log.info("线程" + Thread.currentThread().getName()+"执行结束");}},"B");// 启动线程AthreadA.start();try {// 等待200ms,让线程B获取资源,在这200ms期间A就被阻塞了,释放了obj对象锁Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}// 启动线程BthreadB.start();}
}
执行结果:
线程A开始执行
线程B开始执行
线程B执行结束
线程A执行结束
可以看到主程序线程A启动之后,休眠了200ms让出cup执行权,线程B开始执行后调用notify()方法对阻塞线程A进行唤醒。
故:当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:(1)其他线程调用了该共享对象的notify()或者notifyAll()方法;(2)其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。
1.3 使用技巧
- wait()方法一般配合while使用
- 被唤醒后会重新竞争锁,之后从上次wait位置重新运行
- while 多次判断,防止在wait这段时间内对象被修改
二、obj.notify(All)()
notify()和notifyAll()方法也是Object里面的方法,Object是所有对象的父类,即所有对象都可以调用notify()和notifyAll()方法。但是线程中共享变量在调用这两个方法前,该线程需要获取到这个共享变量的锁才可以,否则会抛出异常。
1.1 notify() 方法
一个线程调用共享对象的 notify() 方法后,会唤醒一个在该共享变量上调用 wait(...) 系列方法后阻塞的线程。
通知线程:调用notify/notifyAll方法时所在的线程叫做通知线程。
1.1.1 调用notify()或notifyAll()不会释放线程的锁
当线程调用notify()或notifyAll()方法时,它不会释放掉线程持有的锁。
在Java中,每个对象都有一个相关联的锁,也称为监视器锁。当一个线程需要访问被该锁保护的对象时,它必须先获得该锁的所有权。所以只有获得锁的线程才能调用wait()、notify()和notifyAll()方法。
当线程调用notify()或notifyAll()方法时,它仅仅是唤醒等待在该对象上的一个或多个线程,以便它们可以继续执行。它不会释放线程持有的锁。因此,其他线程仍然无法访问被该锁保护的对象,直到调用notify()或notifyAll()方法的线程释放锁资源。
在多线程编程中,必须小心地管理锁,以避免死锁和竞争条件等问题。通常,为了确保线程安全和避免死锁,必须确保在访问共享资源时只有一个线程持有锁。当然,这也需要合理地使用wait()、notify()和notifyAll()方法来协调线程的执行顺序。
值得注意的是:
- 一个共享变量上可能会有多个线程在等待,notify()具体唤醒哪个等待的线程是随机的;
- 被唤醒的线程不能马上从wait()方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,等到唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行;
1.2 notifyAll() 方法
notifyAll() 方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。
1.3 使用技巧
尽量让notify / notifyAll()靠近临界区结束的地方。免得等待线程因为没有获得对象的锁,而又进入等待状态。
三、使用实例
比较经典的就是生产者和消费者的例子。
在生产者消费者模型中,推荐使用notifyAll,因为notify唤醒的线程不确定是生产者或消费者。
public class NotifyWaitDemo {// 共享变量队列的最大容量public static final int MAX_SIZE = 1024;// 共享变量public static Queue queue = new Queue();public static void main(String[] args) {// 生产者Thread producer = new Thread(() -> {// 获取共享变量的锁才能调用wait()方法synchronized (queue) {// 一般wait()都配合着while使用,因为线程唤醒后需要不断地轮循来尝试获取锁while (true) {// 当队列满了之后就挂起当前线程(生产者线程)// 并且,释放通过queue的监视器锁,让消费者对象获取到锁,执行消费逻辑if (queue.size() == MAX_SIZE) {try {// 阻塞生产者线程,并且使当前线程释放掉共享变量的锁queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 空闲则生成元素,并且通知消费线程queue.add();// 唤醒消费者来消费,建议用notifyAll(),因为notify()无法确定会唤醒哪一个线程queue.notifyAll();}}});// 消费者Thread consumer = new Thread(() -> {// 需要先获取锁synchronized (queue) {while (true) {// 当队列已经空了之后就挂起当前线程(消费者线程)// 并且,释放通过queue的监视器锁,让生产者对象获取到锁,执行生产逻辑if (queue.size() == 0) {try {// 阻塞消费者线程,并释放共享对象的锁queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 空闲则消费元素,并且通知生产线程queue.take();queue.notifyAll();}}});// 先执行生产者线程producer.start();try {// 将当前线程睡眠1000ms,让生产者先将队列生产满,然后wait阻塞起来,并且释放持有的锁。为了后续能执行消费者线程Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 执行消费者线程consumer.start();}// 共享变量static class Queue {private int size = 0;public int size() {return this.size;}// 生产操作public void add() {// TODOsize++;System.out.println("执行add 操作,current size: " + size);}// 消费操作public void take() {// TODOsize--;System.out.println("执行take 操作,current size: " + size);}}
}
四、wait()/notify()/notifyAll() 为什么定义在 Object 类中?
由于Thread类继承了Object类,所以Thread也可以调用者三个方法,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在Object类中。
相关文章:【并发基础】一篇文章带你彻底搞懂睡眠、阻塞、挂起、终止之间的区别
【并发基础】Java中线程的创建和运行以及相关源码分析
【并发基础】线程,进程,协程的详细解释
【并发基础】操作系统中线程/进程的生命周期与状态流转以及Java线程的状态流转详解