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

写在前面:无论是调用哪种等待和唤醒的方法,都必须是当前线程所持有的对象,否则会导致 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();
}
相关推荐
橘猫云计算机设计4 分钟前
基于springboot微信小程序的旅游攻略系统(源码+lw+部署文档+讲解),源码可白嫖!
java·spring boot·后端·微信小程序·毕业设计·旅游
Yeauty4 分钟前
Rust 中的高效视频处理:利用硬件加速应对高分辨率视频
开发语言·rust·ffmpeg·音视频·音频·视频
落榜程序员5 分钟前
Java 基础-30-单例设计模式:懒汉式与饿汉式
java·开发语言
顾林海5 分钟前
深度解析ArrayList工作原理
android·java·面试
雷渊7 分钟前
spring-IoC容器启动流程源码分析
java·后端·面试
划水哥~9 分钟前
创建QMainWindow菜单栏
开发语言·c++·qt
矿渣渣9 分钟前
int main(int argc, char **argv)C语言主函数参数解析
c语言·开发语言
用户33154891110712 分钟前
一招搞定Java线程池炸弹,系统吞吐量暴增10倍!
java·后端
阿让啊13 分钟前
bootloader+APP中,有些APP引脚无法正常使用?
c语言·开发语言·stm32·单片机·嵌入式硬件
努力的搬砖人.16 分钟前
maven如何使用
java·后端·面试·maven