探索JAVA并发

Wesley13
• 阅读 508

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

简介

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

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

sleep和wait

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

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

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

import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    // 日志行号记录
    private AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();
        // 开启两个线程去执行test方法
        new Thread(main::test).start();
        new Thread(main::test).start();
    }

    private synchronized void test() {
        try {
            log("进入了同步方法,并开始睡觉,1s");
            // sleep不会释放锁,因此其他线程不能进入这个方法
            Thread.sleep(1000);
            log("睡好了,但没事做,有事叫我,等待2s");
            //阻塞在此,并且释放锁,其它线程可以进入这个方法
            //当其它线程调用此对象的notify或者notifyAll时才有机会停止阻塞
            //就算没有人notify,如果超时了也会停止阻塞
            wait(2000);
            log("我要走了,但我要再睡一觉,10s");
            //这里睡的时间很长,因为没有释放锁,其它线程就算wait超时了也无法继续执行
            Thread.sleep(10000);
            log("走了");
            notify();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 打印日志
    private void log(String s) {
        System.out.println(count.incrementAndGet() + " "
                + new Date().toString().split(" ")[3]
                + "\t" + Thread.currentThread().getName() + " " + s);
    }
}

/* 输出:

1 00:13:23    Thread-0 进入了同步方法,并开始睡觉,1s
2 00:13:24    Thread-0 睡好了,但没事做,有事叫我,等待2s
3 00:13:24    Thread-1 进入了同步方法,并开始睡觉,1s
4 00:13:25    Thread-1 睡好了,但没事做,有事叫我,等待2s
5 00:13:26    Thread-0 我要走了,但我要再睡一觉,10s
6 00:13:36    Thread-0 走了
7 00:13:36    Thread-1 我要走了,但我要再睡一觉,10s
8 00:13:46    Thread-1 走了

*/

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

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

notify和notifyAll

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

区别在于:

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

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

import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    private AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();
        // 开启两个线程去执行test方法
        for (int i = 0; i < 10; i++) {
            new Thread(main::testWait).start();
        }
        Thread.sleep(1000);
        for (int i = 0; i < 5; i++) {
            main.testNotify();
        }
    }

    private synchronized void testWait() {
        try {
            log("进入了同步方法,开始wait");
            wait();
            log("wait结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private synchronized void testNotify() {
        notify();
    }

    private void log(String s) {
        System.out.println(count.incrementAndGet() + " "
                + new Date().toString().split(" ")[3]
                + "\t" + Thread.currentThread().getName() + " " + s);
    }

}

/* 输出:

1 00:59:32    Thread-0 进入了同步方法,开始wait
2 00:59:32    Thread-9 进入了同步方法,开始wait
3 00:59:32    Thread-8 进入了同步方法,开始wait
4 00:59:32    Thread-7 进入了同步方法,开始wait
5 00:59:32    Thread-6 进入了同步方法,开始wait
6 00:59:32    Thread-5 进入了同步方法,开始wait
7 00:59:32    Thread-4 进入了同步方法,开始wait
8 00:59:32    Thread-3 进入了同步方法,开始wait
9 00:59:32    Thread-2 进入了同步方法,开始wait
10 00:59:32    Thread-1 进入了同步方法,开始wait
11 00:59:33    Thread-0 wait结束
12 00:59:33    Thread-6 wait结束
13 00:59:33    Thread-7 wait结束
14 00:59:33    Thread-8 wait结束
15 00:59:33    Thread-9 wait结束

*/

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

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

import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    private AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();
        // 开启两个线程去执行test方法
        for (int i = 0; i < 5; i++) {
            new Thread(main::testWait).start();
        }
        Thread.sleep(1000);
        main.testNotifyAll();
    }

    private synchronized void testWait() {
        try {
            log("进入了同步方法,开始wait");
            wait();
            log("wait结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private synchronized void testNotifyAll() {
        notifyAll();
    }

    private void log(String s) {
        System.out.println(count.incrementAndGet() + " "
                + new Date().toString().split(" ")[3]
                + "\t" + Thread.currentThread().getName() + " " + s);
    }

}

/* 输出:

1 01:03:24    Thread-0 进入了同步方法,开始wait
2 01:03:24    Thread-4 进入了同步方法,开始wait
3 01:03:24    Thread-3 进入了同步方法,开始wait
4 01:03:24    Thread-2 进入了同步方法,开始wait
5 01:03:24    Thread-1 进入了同步方法,开始wait
6 01:03:25    Thread-1 wait结束
7 01:03:25    Thread-2 wait结束
8 01:03:25    Thread-3 wait结束
9 01:03:25    Thread-4 wait结束
10 01:03:25    Thread-0 wait结束

*/

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

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

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

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

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

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

探索JAVA并发

点赞
收藏
评论区
推荐文章
秃头王路飞 秃头王路飞
4个月前
webpack5手撸vue2脚手架
webpack5手撸vue相信工作个12年的小伙伴们在面试的时候多多少少怕被问到关于webpack方面的知识,本菜鸟最近闲来无事,就尝试了手撸了下vue2的脚手架,第一次发帖实在是没有经验,望海涵。languageJavaScript"name":"vuecliversion2","version":"1.0.0","desc
技术小男生 技术小男生
4个月前
linux环境jdk环境变量配置
1:编辑系统配置文件vi/etc/profile2:按字母键i进入编辑模式,在最底部添加内容:JAVAHOME/opt/jdk1.8.0152CLASSPATH.:$JAVAHOME/lib/dt.jar:$JAVAHOME/lib/tools.jarPATH$JAVAHOME/bin:$PATH3:生效配置
光头强的博客 光头强的博客
4个月前
Java面向对象试题
1、请创建一个Animal动物类,要求有方法eat()方法,方法输出一条语句“吃东西”。创建一个接口A,接口里有一个抽象方法fly()。创建一个Bird类继承Animal类并实现接口A里的方法输出一条有语句“鸟儿飞翔”,重写eat()方法输出一条语句“鸟儿吃虫”。在Test类中向上转型创建b对象,调用eat方法。然后向下转型调用eat()方
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
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
小森森 小森森
4个月前
校园表白墙微信小程序V1.0 SayLove -基于微信云开发-一键快速搭建,开箱即用
后续会继续更新,敬请期待2.0全新版本欢迎添加左边的微信一起探讨!项目地址:(https://www.aliyun.com/activity/daily/bestoffer?userCodesskuuw5n)\2.Bug修复更新日历2.情侣脸功能大家不要使用了,现在阿里云的接口已经要收费了(土豪请随意),\\和注意
晴空闲云 晴空闲云
4个月前
css中box-sizing解放盒子实际宽高计算
我们知道传统的盒子模型,如果增加内边距padding和边框border,那么会撑大整个盒子,造成盒子的宽度不好计算,在实务中特别不方便。boxsizing可以设置盒模型的方式,可以很好的设置固定宽高的盒模型。盒子宽高计算假如我们设置如下盒子:宽度和高度均为200px,那么这会这个盒子实际的宽高就都是200px。但是当我们设置这个盒子的边框和内间距的时候,那
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Wesley13 Wesley13
1年前
JAVA 并发编程之二:Object对象中的wait,notify,notifyAll 概念+作用(线程状态控制Type1)
<divclass"htmledit\_views"id"content\_views"<pwait,notify,notifyAll是定义在Object类的实例方法,用于控制线程状态。</p<p三个方法都必须在synchronized同步关键字所限定的作用域中调用,否则会报错java.lang.IllegalMonitorStat
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue