探索JAVA并发

> sleep/wait/notify/notifyAll分别有什么作用?它们的区别是什么?wait时为什么要放在循环里而不能直接用if?

简介

首先对几个相关的方法做个简单解释,Object中有几个用于线程同步的方法:wait、notify、notifyAll。

1public class Object { 2 public final native void wait(long timeout) throws InterruptedException; 3 public final native void notify(); 4 public final native void notifyAll(); 5}
  • wait: 释放当前锁,阻塞直到被notify或notifyAll唤醒,或者超时,或者线程被中断(InterruptedException)
  • notify: 任意选择一个(无法控制选哪个)正在这个对象上等待的线程把它唤醒,其它线程依然在等待被唤醒
  • notifyAll: 唤醒所有线程,让它们去竞争,不过也只有一个能抢到锁
  • sleep: 不是Object中的方法,而是Thread类的静态方法,让当前线程持有锁阻塞指定时间

sleep和wait

sleep和wait都可以让线程阻塞,也都可以指定超时时间,甚至还都会抛出中断异常InterruptedException。

而它们最大的区别就在于,sleep时线程依然持有锁,别人无法进当前同步方法;wait时放弃了持有的锁,其它线程有机会进入该同步方法。多次提到同步方法,因为wait必须在synchronized同步代码块中,否则会抛出异常IllegalMonitorStateException,notify也是如此,可以说wait和notify是就是为了在同步代码中做线程调度而生的。

下面一个简单的例子展现sleep和wait的区别:

1import java.util.Date; 2import java.util.concurrent.atomic.AtomicInteger; 3 4public class Main { 5 6 // 日志行号记录 7 private AtomicInteger count = new AtomicInteger(); 8 9 public static void main(String[] args) throws InterruptedException { 10 Main main = new Main(); 11 // 开启两个线程去执行test方法 12 new Thread(main::test).start(); 13 new Thread(main::test).start(); 14 } 15 16 private synchronized void test() { 17 try { 18 log("进入了同步方法,并开始睡觉,1s"); 19 // sleep不会释放锁,因此其他线程不能进入这个方法 20 Thread.sleep(1000); 21 log("睡好了,但没事做,有事叫我,等待2s"); 22 //阻塞在此,并且释放锁,其它线程可以进入这个方法 23 //当其它线程调用此对象的notify或者notifyAll时才有机会停止阻塞 24 //就算没有人notify,如果超时了也会停止阻塞 25 wait(2000); 26 log("我要走了,但我要再睡一觉,10s"); 27 //这里睡的时间很长,因为没有释放锁,其它线程就算wait超时了也无法继续执行 28 Thread.sleep(10000); 29 log("走了"); 30 notify(); 31 } catch (InterruptedException e) { 32 e.printStackTrace(); 33 } 34 } 35 36 // 打印日志 37 private void log(String s) { 38 System.out.println(count.incrementAndGet() + " " 39 + new Date().toString().split(" ")[3] 40 + "\t" + Thread.currentThread().getName() + " " + s); 41 } 42} 43 44/* 输出: 45 461 00:13:23 Thread-0 进入了同步方法,并开始睡觉,1s 472 00:13:24 Thread-0 睡好了,但没事做,有事叫我,等待2s 483 00:13:24 Thread-1 进入了同步方法,并开始睡觉,1s 494 00:13:25 Thread-1 睡好了,但没事做,有事叫我,等待2s 505 00:13:26 Thread-0 我要走了,但我要再睡一觉,10s 516 00:13:36 Thread-0 走了 527 00:13:36 Thread-1 我要走了,但我要再睡一觉,10s 538 00:13:46 Thread-1 走了 54 55*/

对输出做个简单解释(已经看懂代码的童鞋可以跳过):

11 00:13:23 Thread-0 进入了同步方法,并开始睡觉,1s // Thread-0首先进入同步方法,Thread-1只能门外候着 22 00:13:24 Thread-0 睡好了,但没事做,有事叫我,等待2s // Thread-0 sleep 1秒这段时间,Thread-1没进来,证明sleep没有释放锁 33 00:13:24 Thread-1 进入了同步方法,并开始睡觉,1s // Thread-0开始wait后Thread-1马上就进来了,证明wait释放了锁 44 00:13:25 Thread-1 睡好了,但没事做,有事叫我,等待2s // Thread-1也打算wait 2秒(2秒后真的能醒来吗?) 55 00:13:26 Thread-0 我要走了,但我要再睡一觉,10s // Thread-0已经wait超时醒来了,这次准备sleep 10s 66 00:13:36 Thread-0 走了 // 10s过去了Thread-0都sleep结束了,那个说要wait 2s的Thread-1还没动静,证明超时也没用,还得抢到锁 77 00:13:36 Thread-1 我要走了,但我要再睡一觉,10s // Thread-0退出同步代码后,Thread-1才终于得到了锁,能行动了 88 00:13:46 Thread-1 走了

notify和notifyAll

同样是唤醒等待的线程,同样最多只有一个线程能获得锁,同样不能控制哪个线程获得锁。

区别在于:

  • notify:唤醒一个线程,其他线程依然处于wait的等待唤醒状态,如果被唤醒的线程结束时没调用notify,其他线程就永远没人去唤醒,只能等待超时,或者被中断
  • notifyAll:所有线程退出wait的状态,开始竞争锁,但只有一个线程能抢到,这个线程执行完后,其他线程又会有一个幸运儿脱颖而出得到锁

如果觉得解释的不够明白,代码来一波:

1import java.util.Date; 2import java.util.concurrent.ArrayBlockingQueue; 3import java.util.concurrent.atomic.AtomicInteger; 4 5public class Main { 6 7 private AtomicInteger count = new AtomicInteger(); 8 9 public static void main(String[] args) throws InterruptedException { 10 Main main = new Main(); 11 // 开启两个线程去执行test方法 12 for (int i = 0; i < 10; i++) { 13 new Thread(main::testWait).start(); 14 } 15 Thread.sleep(1000); 16 for (int i = 0; i < 5; i++) { 17 main.testNotify(); 18 } 19 } 20 21 private synchronized void testWait() { 22 try { 23 log("进入了同步方法,开始wait"); 24 wait(); 25 log("wait结束"); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 } 30 31 private synchronized void testNotify() { 32 notify(); 33 } 34 35 private void log(String s) { 36 System.out.println(count.incrementAndGet() + " " 37 + new Date().toString().split(" ")[3] 38 + "\t" + Thread.currentThread().getName() + " " + s); 39 } 40 41} 42 43/* 输出: 44 451 00:59:32 Thread-0 进入了同步方法,开始wait 462 00:59:32 Thread-9 进入了同步方法,开始wait 473 00:59:32 Thread-8 进入了同步方法,开始wait 484 00:59:32 Thread-7 进入了同步方法,开始wait 495 00:59:32 Thread-6 进入了同步方法,开始wait 506 00:59:32 Thread-5 进入了同步方法,开始wait 517 00:59:32 Thread-4 进入了同步方法,开始wait 528 00:59:32 Thread-3 进入了同步方法,开始wait 539 00:59:32 Thread-2 进入了同步方法,开始wait 5410 00:59:32 Thread-1 进入了同步方法,开始wait 5511 00:59:33 Thread-0 wait结束 5612 00:59:33 Thread-6 wait结束 5713 00:59:33 Thread-7 wait结束 5814 00:59:33 Thread-8 wait结束 5915 00:59:33 Thread-9 wait结束 60 61*/

例子中有10个线程在wait,但notify了5次,然后其它线程一直阻塞,这也就说明使用notify时如果不能准确控制和wait的线程数对应,可能会导致某些线程永远阻塞。

使用notifyAll唤醒所有等待的线程:

1import java.util.Date; 2import java.util.concurrent.ArrayBlockingQueue; 3import java.util.concurrent.atomic.AtomicInteger; 4 5public class Main { 6 7 private AtomicInteger count = new AtomicInteger(); 8 9 public static void main(String[] args) throws InterruptedException { 10 Main main = new Main(); 11 // 开启两个线程去执行test方法 12 for (int i = 0; i < 5; i++) { 13 new Thread(main::testWait).start(); 14 } 15 Thread.sleep(1000); 16 main.testNotifyAll(); 17 } 18 19 private synchronized void testWait() { 20 try { 21 log("进入了同步方法,开始wait"); 22 wait(); 23 log("wait结束"); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 } 28 29 private synchronized void testNotifyAll() { 30 notifyAll(); 31 } 32 33 private void log(String s) { 34 System.out.println(count.incrementAndGet() + " " 35 + new Date().toString().split(" ")[3] 36 + "\t" + Thread.currentThread().getName() + " " + s); 37 } 38 39} 40 41/* 输出: 42 431 01:03:24 Thread-0 进入了同步方法,开始wait 442 01:03:24 Thread-4 进入了同步方法,开始wait 453 01:03:24 Thread-3 进入了同步方法,开始wait 464 01:03:24 Thread-2 进入了同步方法,开始wait 475 01:03:24 Thread-1 进入了同步方法,开始wait 486 01:03:25 Thread-1 wait结束 497 01:03:25 Thread-2 wait结束 508 01:03:25 Thread-3 wait结束 519 01:03:25 Thread-4 wait结束 5210 01:03:25 Thread-0 wait结束 53 54*/

只需要调用一次notifyAll,所有的等待线程都被唤醒,并且去竞争锁,然后依次(无序)获取锁完成了后续任务。

为什么wait要放到循环中使用

一些源码中出现wait时,往往都是伴随着一个循环语句出现的,比如:

1private synchronized void f() throws InterruptedException { 2 while (!isOk()) { 3 wait(); 4 } 5 System.out.println("I'm ok"); 6}

既然wait会被阻塞直到被唤醒,那么用if+wait不就可以了吗?其他线程发现条件达到时notify一下不就行了?

理想情况确实如此,但实际开发中我们往往不能保证这个线程被notify时条件已经满足了,因为很可能有某个无关(和这个条件的逻辑无关)的线程因为需要线程调度而调用了notify或者notifyAll。此时如果样例中位置等待的线程不巧被唤醒,它就会继续往下执行,但因为用的if,这次被唤醒就不会再判断条件是否满足,最终程序按照我们不期望的方式执行下去。

acupjava

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Java并发 wait()、notify()和notifyAll()

一个线程修改一个对象的值,而另一个线程则感知到了变化,然后进行相应的操作,这就是wait()、notify()和notifyAll()方法的本质。具体体现到方法上则是这样的:一个线程A调用了对象obj的wait方法进入到等待状态,而另一个线程调用了对象obj的notify()或者notifyAll()方法,线程A收到通知后从对象obj的wait方法返回,继续

探索JAVA并发 - HelloWorld