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

写在前面:无论是调用哪种等待和唤醒的方法,都必须是当前线程所持有的对象,否则会导致 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();
}
相关推荐
嘿嘿-665 分钟前
Windows 一键使用 GPT-6 Astra:Codex CLI 配置教程
java·人工智能·windows·gpt·chatgpt·web
统计学小王子35 分钟前
数学建模国赛倒计时4天——《统计模型精讲(混合效应模型R语言实现)》
开发语言·数学建模·r语言
知无不研39 分钟前
c语言中循环的介绍与简单应用
c语言·开发语言·算法·循环·for·while
郝学胜-神的一滴44 分钟前
Qt 高级编程 045:坐标体系深度实战
开发语言·c++·windows·python·qt·程序人生
dsyyyyy11011 小时前
JS复习,难点,易错点总结
开发语言·javascript·ecmascript
策案案案1 小时前
YOLO: 将AI Agents嵌入到IntelliJ IDEA
java·大数据·人工智能·笔记·yolo·microsoft·intellij-idea
“AI国潮设计-小江”2 小时前
Python实战 | SDXL精准控制“普宁英歌舞×星空蛋糕”IP落地,附核心Prompt与商用授权思路
开发语言·人工智能·python·prompt·aigc
辰烨chenye3 小时前
LeetCode Hot 100 题解 · 堆篇
java·算法·leetcode
shehuiyuelaiyuehao3 小时前
算法29,前缀和,除自身以外的数组的乘积
java·数据结构·算法
小刘在重生~3 小时前
Java常用类|String类详解 + BigDecimal精准计算(含面试题)
java·开发语言·python