线程等待与唤醒的几种方法与注意事项

写在前面:无论是调用哪种等待和唤醒的方法,都必须是当前线程所持有的对象,否则会导致 java.lang.IllegalMonitorStateException 等并发安全问题。

以三个线程循环打印 XYZ 为例。

一、方法

1.1 Object 对象锁

可以通过 synchronized 对方法、对象实例、类加锁,并调用加锁对象的 Object#wait() (会释放线程持有的锁)和 Object#notify() 方法等待和唤醒线程。

java 复制代码
class Main {
    // 打印次数
    private static final int times = 10;
    // 下一个打印的字母类型
    private static volatile int type = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            int v = i;
            new Thread(() -> print(v)).start();
        }
    }

    /**
     * curType:当前线程打印的类型
     * 对静态方法加锁,锁住的是类本身
     */
    private static synchronized void print(int curType) {
        for (int i = 0; i < times; ) {
            try {
                // 如果当前类型不是自己的类型,则等待
                while (type != curType) {
                    Main.class.wait();
                }
                char c = (char) ('X' + curType);
                System.out.print(c);
                type = (type + 1) % 3;
                i++;
                // 唤醒全部线程
                Main.class.notifyAll();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

1.2 Lock#Condition 类

Condition 类与 Lock 类配合使用,允许多个 Condition 和一个 Lock 关联,提供了更加灵活强大的线程同步机制。

java 复制代码
class Main {
    // 打印次数
    private static final int times = 10;
    // 下一个打印的字母类型
    private static volatile int type = 0;
    private static Lock lock = new ReentrantLock();
    private static Condition[] conditions = new Condition[3];

    static {
        for (int i = 0; i < 3; i++) {
            conditions[i] = lock.newCondition();
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            int v = i;
            new Thread(() -> print(v)).start();
        }
    }

    /**
     * curType:当前线程打印的类型
     */
    private static void print(int curType) {
        for (int i = 0; i < times; ) {
            lock.lock();
            try {
                // 如果当前类型不是自己的类型,则等待
                while (type != curType) {
                    conditions[curType].await();
                }
                char c = (char) ('X' + curType);
                System.out.print(c);
                type = (type + 1) % 3;
                i++;
                // 唤醒下一个线程
                conditions[type].signal();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        }
    }
}

1.3 Semaphore

1.4 CyclicBarrier

1.5 CountDownLatch

二、注意事项

2.1 虚假唤醒

java 复制代码
class Main {
    // 打印次数
    private static final int times = 10;
    // 下一个打印的字母类型
    private static volatile int type = 0;

    public static void main(String[] args) {
        Main1 main = new Main1();
        for (int i = 0; i < 3; i++) {
            int v = i;
            new Thread(() -> main.print(v)).start();
        }
    }

    private synchronized void print(int curType) {
        for (int i = 0; i < times; ) {
            try {
                // 如果当前类型不是自己的类型,则等待
                if (type != curType) {
                    wait();
                }
                char c = (char) ('X' + curType);
                System.out.print(c);
                type = (type + 1) % 3;
                i++;
                // 唤醒全部线程
                notifyAll();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

大家可以执行一下这段代码,会发现打印出来的结果是乱序的,问题的原因就是发生了虚假唤醒。

所谓虚假唤醒,指的是线程在没有满足唤醒条件的情况下被唤醒,发生的原因(排除自身代码逻辑问题)主要是内核线程调度器的调度策略不当(出于性能和效率的考量,会提前唤醒某些线程)。

而只需要把这里改成 while 循环,在线程被唤醒后再检查一遍是否满足唤醒条件即可。

java 复制代码
while (type != curType) {
    wait();
}

2.2 IllegalMonitorStateException 异常原因

调用等待和唤醒方法的线程没有持有对应的锁。

java 复制代码
// 正确
Object lock = new Object();
synchronized(lock){
    lock.wait();
}
 
// 错误,this.wait() 关联的是当前对象实例的锁,而不是 lock 实例
// 当前线程并未对当前对象实例加锁,抛出 IllegalMonitorStateException 异常
Object lock = new Object();
synchronized(lock){
    this.wait();
}
相关推荐
笑虾12 小时前
Win10 修改注册表 让鼠标悬停PNG上时 tip 始终显示分辨率
开发语言·javascript·ecmascript
lolo大魔王12 小时前
Go语言的并发、协调创建和通信机制
开发语言·golang
xxyy88812 小时前
关于labelimg安装后在标注过程中闪退和死机的问题处理
开发语言·python
JAVA面经实录91712 小时前
Java开发工程基础完整手册(企业实战完整版)
java·开发语言·git·ci/cd·svn·github·intellij idea
李艺为12 小时前
Fake Device Test作假屏幕分辨率分析
android·java
无敌的黑星星12 小时前
Spring @Transactional 注解全解析
java·数据库·oracle
xiaogg367812 小时前
spring oauth2 单点登录
java·vue.js·spring
skywalk816312 小时前
【文言心】- 中文编程语言> 一门追求同像性(Homoiconicity)的中文编程语言,代码即数据,数据即代码。
开发语言
南境十里·墨染春水12 小时前
C++笔记 forward完美转发
开发语言·c++·笔记
代码羊羊12 小时前
Rust 格式化输出完全攻略:从入门到精通
开发语言·后端·rust