一、前言

前情简介:

java 并发——内置锁

java 并发——线程

java 面试是否有被问到过,sleepwait 方法的区别,关于这个问题其实不用多说,大多数人都能回答出最主要的两点区别:

  • sleep 是线程的方法, wait / notify / notifyAll 是 Object 类的方法;
  • sleep 不会释放当前线程持有的锁,到时间后程序会继续执行,wait 会释放线程持有的锁并挂起,直到通过 notify 或者 notifyAll 重新获得锁。

    另外还有一些参数、异常等区别,不细说了。本文重点记录一下 wait / notify / notifyAll 的相关知识。

二、常见的同步场景

开发中常常遇到这样的场景:

  1. 一个线程执行过程中,需要开启另外一个子线程去做某个耗时的操作(通过休眠3秒模拟),
  2. 并且**等待**子线程返回结果,主线程再根据返回的结果继续往下执行。

这里注意我上面加*两个字“等待”。如果不需要等待,单纯只是对子线程的结果做处理,我们大可注册回调方法解决问题,此文不再赘述接口回调。

此处场景就是主线程停下来等待子线程执行完毕后,主线程再继续执行。针对该场景下面给出实现:

设置一个判断的标志位

  1. volatile boolean flag = false;
  2. public void test(){
  3. //...
  4. Thread t1 = new Thread(() -> {
  5. try {
  6. Thread.sleep(3000);
  7. System.out.println("--- 休眠 3 秒");
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. } finally {
  11. flag = true;
  12. }
  13. });
  14. t1.start();
  15. while(!flag){
  16. }
  17. System.out.println("--- work thread run");
  18. }

上面的代码,执行结果:

强调一点,声明标志位的时候,一定注意 volatile 关键字不能忘,如果不加该关键字修饰,程序可能进入死循环。这是同步中的可见性问题,在 《java 并发——内置锁》 中有记录。

显然,这个实现方案并不好,本来主线程什么也不用做,却一直在竞争资源,做空循环,性能上不好,所以并不推荐。

线程的 join 方法

  1. public void test(){
  2. //...
  3. Thread t1 = new Thread(() -> {
  4. try {
  5. Thread.sleep(3000);
  6. System.out.println("--- 休眠 3 秒");
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. });
  11. t1.start();
  12. try {
  13. t1.join();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. System.out.println("--- work thread continue");
  18. }

上面的代码,执行结果同上。利用 Thread 类的 join 方法实现了同步,达到了效果,但是 join 方法不能一定保证效果,在不同的 cpu 上,可能呈现出意想不到的结果,所以尽量不要用上述方法。

使用闭锁 CountDownLatch

不清楚闭锁的新同学可点击文章开头给出的另一篇文章,《java 并发——线程》。

  1. public void test(){
  2. //...
  3. final CountDownLatch countDownLatch = new CountDownLatch(1);
  4. Thread t1 = new Thread(() -> {
  5. try {
  6. Thread.sleep(3000);
  7. System.out.println("--- 休眠 3 秒");
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. } finally {
  11. countDownLatch.countDown();
  12. }
  13. });
  14. t1.start();
  15. try {
  16. countDownLatch.await();
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. System.out.println("--- work thread run");
  21. }

上面的代码,执行结果同上。同样可以实现上述效果,执行结果和上面一样。该方法推荐使用。

利用 wait / notify 优化标志位方法

为了方便对比,首先给 2.1 中的循环方法增加一些打印。修改后的代码如下:

  1. volatile boolean flag = false;
  2. public void test() {
  3. //...
  4. Thread t1 = new Thread(() -> {
  5. try {
  6. Thread.sleep(3000);
  7. System.out.println("--- 休眠 3 秒");
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. } finally {
  11. flag = true;
  12. }
  13. });
  14. t1.start();
  15. while (!flag) {
  16. try {
  17. System.out.println("---while-loop---");
  18. Thread.sleep(500);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. System.out.println("--- work thread run");
  24. }

执行结果如下:

事实证明,while 循环确实一直在执行。

为了使该线程再不需要执行的时候不抢占资源,我们可以利用 wait 方法将其挂起,在需要它执行的时候,再利用 notify 方法将其唤醒。这样达到优化的目的,优化后的代码如下:

  1. volatile boolean flag = false;
  2. public void test() {
  3. //...
  4. final Object obj = new Object();
  5. Thread t1 = new Thread(() -> {
  6. synchronized (obj) {
  7. try {
  8. Thread.sleep(3000);
  9. System.out.println("--- 休眠 3 秒");
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. } finally {
  13. flag = true;
  14. }
  15. obj.notify();
  16. }
  17. });
  18. t1.start();
  19. synchronized (obj) {
  20. while (!flag) {
  21. try {
  22. System.out.println("---while-loop---");
  23. Thread.sleep(500);
  24. obj.wait();
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }
  30. System.out.println("--- work thread run");
  31. }

执行结果:

结果证明,优化后的程序,循环只执行了一次。

三、理解 wait / notify / notifyAll

在Java中,每个对象都有两个池,锁(monitor)池和等待池

锁池

锁池:假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

notify 和 notifyAll 的区别

wait()

public final void wait() throws InterruptedException,IllegalMonitorStateException

该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 结

notify()

public final native void notify() throws IllegalMonitorStateException

该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。

该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。这与下面的 notifyAll()方法执行后的情况不同。

notifyAll()

public final native void notifyAll() throws IllegalMonitorStateException

该方法与 notify ()方法的工作方式相同,重要的一点差异是:

notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

四、生产者与消费者模式

生产者与消费者问题是并发编程里面的经典问题。接下来说说利用wait()和notify()来实现生产者和消费者并发问题:

显然要保证生产者和消费者并发运行不出乱,主要要解决:当生产者线程的缓存区为满的时候,就应该调用wait()来停止生产者继续生产,而当生产者满的缓冲区被消费者消费掉一块时,则应该调用notify()唤醒生产者,通知他可以继续生产;同样,对于消费者,当消费者线程的缓存区为空的时候,就应该调用wait()停掉消费者线程继续消费,而当生产者又生产了一个时就应该调用notify()来唤醒消费者线程通知他可以继续消费了。

下面是一个简单的代码实现:

  1. package com.sharpcj;
  2. import java.util.Random;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. public class Test {
  6. public static void main(String[] args) {
  7. Reposity reposity = new Reposity(600);
  8. ExecutorService threadPool = Executors.newCachedThreadPool();
  9. for(int i = 0; i < 10; i++){
  10. threadPool.submit(new Producer(reposity));
  11. }
  12. for(int i = 0; i < 10; i++){
  13. threadPool.submit(new Consumer(reposity));
  14. }
  15. threadPool.shutdown();
  16. }
  17. }
  18. class Reposity {
  19. private static final int MAX_NUM = 2000;
  20. private int currentNum;
  21. private final Object obj = new Object();
  22. public Reposity(int currentNum) {
  23. this.currentNum = currentNum;
  24. }
  25. public void in(int inNum) {
  26. synchronized (obj) {
  27. while (currentNum + inNum > MAX_NUM) {
  28. try {
  29. System.out.println("入货量 " + inNum + " 线程 " + Thread.currentThread().getId() + "被挂起...");
  30. obj.wait();
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. try {
  36. Thread.sleep(200);
  37. } catch (InterruptedException e) {
  38. e.printStackTrace();
  39. }
  40. currentNum += inNum;
  41. System.out.println("线程: " + Thread.currentThread().getId() + ",入货:inNum = [" + inNum + "], currentNum = [" + currentNum + "]");
  42. obj.notifyAll();
  43. }
  44. }
  45. public void out(int outNum) {
  46. synchronized (obj) {
  47. while (currentNum < outNum) {
  48. try {
  49. System.out.println("出货量 " + outNum + " 线程 " + Thread.currentThread().getId() + "被挂起...");
  50. obj.wait();
  51. } catch (InterruptedException e) {
  52. e.printStackTrace();
  53. }
  54. }
  55. try {
  56. Thread.sleep(200);
  57. } catch (InterruptedException e) {
  58. e.printStackTrace();
  59. }
  60. currentNum -= outNum;
  61. System.out.println("线程: " + Thread.currentThread().getId() + ",出货:outNum = [" + outNum + "], currentNum = [" + currentNum + "]");
  62. obj.notifyAll();
  63. }
  64. }
  65. }
  66. class Producer implements Runnable {
  67. private Reposity reposity;
  68. public Producer(Reposity reposity) {
  69. this.reposity = reposity;
  70. }
  71. @Override
  72. public void run() {
  73. reposity.in(200);
  74. }
  75. }
  76. class Consumer implements Runnable {
  77. private Reposity reposity;
  78. public Consumer(Reposity reposity) {
  79. this.reposity = reposity;
  80. }
  81. @Override
  82. public void run() {
  83. reposity.out(200);
  84. }
  85. }

执行结果:

五、写在后面

最后做几点总结:

  1. 调用wait方法和notify、notifyAll方法前必须获得对象锁,也就是必须写在synchronized(锁对象){......}代码块中。

  2. 当线程调用了wait方法后就释放了对象锁,否则其他线程无法获得对象锁。

  3. 当调用 wait() 方法后,线程必须再次获得对象锁后才能继续执行。

  4. 如果另外两个线程都在 wait,则正在执行的线程调用notify方法只能唤醒一个正在wait的线程(公平竞争,由JVM决定)。

  5. 当使用notifyAll方法后,所有wait状态的线程都会被唤醒,但是只有一个线程能获得锁对象,必须执行完while(condition){this.wait();}后才释放对象锁。其余的需要等待该获得对象锁的线程执行完释放对象锁后才能继续执行。

  6. 当某个线程调用notifyAll方法后,虽然其他线程被唤醒了,但是该线程依然持有着对象锁,必须等该同步代码块执行完(右大括号结束)后才算正式释放了锁对象,另外两个线程才有机会执行。

  7. 第5点中说明, wait 方法的调用前的条件判断需放在循环中,否则可能出现逻辑错误。另外,根据程序逻辑合理使用 wait 即 notify 方法,避免如先执行 notify ,后执行 wait 方法,线程一直挂起之类的错误。

java 并发——理解 wait / notify / notifyAll的更多相关文章

  1. java中的wait(),notify(),notifyAll(),synchronized方法

    wait(),notify(),notifyAll()三个方法不是Thread的方法,而是Object的方法.意味着所有对象都有这三个方法,因为每个对象都有锁,所以自然也都有操作锁的方法了.这三个方法 ...

  2. Java多线程的wait(),notify(),notifyAll()

    在多线程的情况下.因为多个线程与存储空间共享相同的过程,同时带来的便利.它也带来了访问冲突这个严重的问题. Java语言提供了一种特殊的机制来解决这类冲突,避免同一数据对象由多个线程在同一时间访问. ...

  3. Java多线程:wait(),notify(),notifyAll()

    1. wait(),notify(),notifyAll() 2. wait() 2.1. wait() 2.2. wait(long timeout) 2.3. wait(long timeout, ...

  4. java 多线程(wait/notify/notifyall)

    package com.example; public class App { /* wait\notify\notifyAll 都属于object的内置方法 * wait: 持有该对象的线程把该对象 ...

  5. Java并发编程_wait/notify和CountDownLatch的比较(三)

     1.wait/notify方法 package sync; import java.util.ArrayList; import java.util.List; public class WaitA ...

  6. 深入理解 Java 并发锁

    本文以及示例源码已归档在 javacore 一.并发锁简介 确保线程安全最常见的做法是利用锁机制(Lock.sychronized)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程可以执行某个方 ...

  7. 【Java并发编程】并发编程大合集-值得收藏

    http://blog.csdn.net/ns_code/article/details/17539599这个博主的关于java并发编程系列很不错,值得收藏. 为了方便各位网友学习以及方便自己复习之用 ...

  8. 多线程学习-基础(六)分析wait()-notify()-notifyAll()

    一.理解wait()-notify()-notifyAll()obj.wait()与obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,notify是针对已经获 ...

  9. 【Java并发编程】并发编程大合集

    转载自:http://blog.csdn.net/ns_code/article/details/17539599 为了方便各位网友学习以及方便自己复习之用,将Java并发编程系列内容系列内容按照由浅 ...

随机推荐

  1. hdu 1233:还是畅通工程(数据结构,图,最小生成树,普里姆(Prim)算法)

    还是畅通工程 Time Limit : 4000/2000ms (Java/Other)   Memory Limit : 65536/32768K (Java/Other) Total Submis ...

  2. jQuery实现提交按钮点击后变成正在处理字样并禁止点击的方法

    本文实例讲述了jQuery实现提交按钮点击后变成正在处理字样并禁止点击的方法.分享给大家供大家参考.具体实现方法如下: 这里主要通过val方法设置按钮的文字,并用attr方法修改disabled属性实 ...

  3. 通过HttpWebRequest在后台对WebService进行调用

    目录: 1 后台调用Webservice的业务需求 2 WebService支持的交互协议 3 如何配置WebService支持的协议 4 后台对WebService的调用 4.1 SOAP 1.1 ...

  4. C语言二维数组

    上节讲解的数组可以看作是一行连续的数据,只有一个下标,称为一维数组.在实际问题中有很多数据是二维的或多维的,因此C语言允许构造多维数组.多维数组元素有多个下标,以确定它在数组中的位置.本节只介绍二维数 ...

  5. codevs 5964 [SDOI2017]序列计数

     [题解] 官方题解就两句话. 写了三个版本的不同分值代码.看代码吧. 前导1 //f[i][j][1/0]表示长为i,sum mod p=j,是否已经选了质数的方案数 #include<cst ...

  6. python练习题-3

    author:headsen chen date: 2018-06-01  15:51:05 习题 31:  作出决定(if + raw_input) [root@localhost py]# cat ...

  7. <span>和<div>标签的隐藏和显示切换

    <div class="axb"> <span id="tipStep1" class="fl" >第一步显示< ...

  8. linux如何查看某个pid的进程?

    Linux通过PID查看进程完整信息 [root@gsidc-4q-saas23 ~]# netstat -anp|grep 8282tcp 0 0 :::8282 :::* LISTEN 16923 ...

  9. 利用jsPerf优化Web应用的性能

    在前端开发的过程中,掌握好浏览器的特性进行有针对性的性能调优是一项基本工作,jsperf.com是一个用来发布基于HTML的针对性能比较的测试用例的网站,你可以在jsPerf上在线填写和运行测试用例, ...

  10. Jsp 公用标签库

    <%@ page language="java" pageEncoding="UTF-8"%> <%@ taglib prefix=" ...