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

写在前面:无论是调用哪种等待和唤醒的方法,都必须是当前线程所持有的对象,否则会导致 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();
}
相关推荐
wt_cs5 分钟前
身份证实名认证接口数字时代的信任基石-node.js实名认证集成
开发语言·node.js·php
普兰店拉马努金19 分钟前
【高中数学/古典概率】4红2黑六选二,求取出两次都是红球的概率
java·概率
智商低情商凑20 分钟前
CAS(Compare And Swap)
java·jvm·面试
yangmf204020 分钟前
使用 Logstash 迁移 MongoDB 数据到 Easysearch
java·elasticsearch·搜索引擎
爱编程的鱼23 分钟前
C# 结构(Struct)
开发语言·人工智能·算法·c#
Tiger_shl24 分钟前
【Python语言基础】24、并发编程
java·数据库·python
FAQEW27 分钟前
Spring boot 中的IOC容器对Bean的管理
java·spring boot·后端·bean·ioc容器
05091533 分钟前
测试基础笔记第十一天
java·数据库·笔记
只可远观34 分钟前
Flutter Dart 循环语句 for while do..while break、continue
开发语言·javascript·ecmascript
IDRSolutions_CN1 小时前
如何将 PDF 中的文本提取为 JSON 格式
java·经验分享·pdf·软件工程·团队开发