【多线程】多线程打印ABC

(一)题目

三个线程,第一个线程打印 A,第二个线程打印 B,第三个线程打印 C

使用这三个线程,交替打印,即 ABCABC...

(二)解法

java 复制代码
public class PrintABCABC {
    private static int state = 0;
    private static int times = 5;
    private static Lock lock = new ReentrantLock();
    private static Condition c1 = lock.newCondition();
    private static Condition c2 = lock.newCondition();
    private static Condition c3 = lock.newCondition();

    private static void printLetter(char c, int flag, Condition current, Condition next)
    {
        lock.lock(); //先获取锁,
        try{
            for(int i = 0; i < times; i ++)
            {
                while(state % 3 != flag) current.await(); //如果不该当前线程操作,释放锁,进入等待队列
                //被唤醒,自动重新获取到锁,执行操作
                System.out.println(Thread.currentThread().getName() + ": " + c);
                state ++;
                //执行完毕,唤醒下一个线程
                next.signal();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); //释放当前锁
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            printLetter('A', 0, c1, c2);
        }, "Thread-A").start();
        new Thread(() -> {
            printLetter('B', 1, c2, c3);
        }, "Thread-B").start();
        new Thread(() -> {
            printLetter('C', 2, c3, c1);
        }, "Thread-C").start();
    }
}

或者将 lock 写在循环里面

java 复制代码
public class PrintABCABC2 {
    private static int state = 0;
    private static int times = 3;
    private static Lock lock = new ReentrantLock();
    private static Condition c1 = lock.newCondition();
    private static Condition c2 = lock.newCondition();
    private static Condition c3 = lock.newCondition();

    public static void printLetter(char c, int flag, Condition current, Condition next)
    {
        for(int i = 0; i < times; i ++)
        {
            lock.lock(); //在每次循环内部加锁
            try {
                while(state % 3 != flag) current.await();
                System.out.println(c);
                state ++;
                next.signal();
            } catch (InterruptedException e){
                e.printStackTrace();
            } finally {
                lock.unlock(); //解锁
            }
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            printLetter('A', 0, c1, c2);
        }, "A").start();
        new Thread(() -> {
            printLetter('B', 1, c2, c3);
        }, "B").start();
        new Thread(() -> {
            printLetter('C', 2, c3, c1);
        }, "C").start();
    }
}

(三)常见问题

(1)应该在循环外部还是循环内部调用 .lock()

两种写法功能上都正确,但工程上更推荐「外部加锁」这种写法。

先记住 Condition.await() 的底层语义:

复制代码
await() = 
	1. 释放当前 lock
	2. 当前线程进入 condition 等待队列
	3. 被 signal 唤醒后
	4. 重新竞争并获取 lock
	5. 从 await 返回

所以,只要线程在 await 之前持有 lock,就一定不会死锁。 这就是为什么两种写法都"能跑对"。

但是,外部加锁版本在语义最合理,更推荐。原因如下:

  • 从并发模型角度看,它表达的是:"整个 for 循环是一个受同一把锁保护的逻辑整体"而不是"我每一轮都重新进出一次锁"。更符合并发代码中的 语义层级一致性
  • Condition 的设计初衷就是"在持有同一把锁的前提下多次 await/signal";
  • 外部加锁减少不必要的 lock/unlock 开销;

几种误解:

  1. 外部加锁 ≠ 一直霸占锁,await 的真实行为不是占有锁睡眠,而是释放锁进入等待队列,此时其他线程可以获取锁。
  2. 不会导致死锁:不存在"持有锁并等待"的情况;
  3. 不会导致其他线程饥饿:饥饿的前提是有线程长期无法获得锁,但是在当前实现中,每个线程打印一次必然 signal 下一个,并且自动释放锁,这是一个强公平协作系统。
(2)Condition.await()lock.unlock() 有什么区别?

两者都会释放锁,不同的是:

  • Condition.await()会将当前线程进入 Condition 的等待队列,接下来的代码不会被执行 。当被 Condition.signal()唤醒时会自动重新获取锁,才会继续执行后面的代码。
  • lock.unlock()不会将当前线程进入等待队列,只是释放锁。当前线程会继续往下执行后面的代码。
(3)为什么使用 while(state % 3 != flag),而不是 if

原因 1:操作系统和 JVM 可能存在 虚假唤醒 ,线程可能在没有任何线程 signal 的情况下,在没有满足等待条件的情况下,被唤醒了。

使用 while,不管线程因为什么原因醒来,都会重新检查一次条件。

原因 2:可能会有信号丢失。信号丢失是说某个线程已经发出了唤醒信号,但目标线程却永远收不到这个信号,从而永久阻塞。用 while 可以避免因信号丢失而导致的错误行为(不能避免信号丢失本身)。

并发中的铁律:wait/await 永远放在 while 里,永远不要用 if

虚假唤醒产生的原因?

  1. 操作系统层面:在内核实现里,一次性唤醒多个线程。多出来的线程就属于"虚假唤醒"。线程在内核中可能因为时钟中断、CPU 抢占被强制唤醒或重新调度。
  2. JVM 层面:JVM 在实现 Condition 时可能合并等待队列、重排序唤醒逻辑等,不保证 signal 的精确性。
    信号丢失产生的原因?

信号丢失发生在 await() 内部的 释放锁但尚未进入 Condition 等待队列 的窗口期(这两个操作在 await 内部不是原子化的),此时其他线程可以获得锁并调用 signal(),但由于等待队列为空,信号被丢弃。

while 并不能解决信号丢失,信号该丢还是会丢。它解决的是"即使信号丢失,程序也不会因此进入错误状态",唤醒之后会再次判断,满足条件才继续执行。而不是像 if,唤醒就直接往下走。

可以解决信号丢失的方案:

要彻底避免信号丢失,唯一的方法是:不用"瞬时信号",而用"可累计 / 可回放的状态"。

比如:CountDownLatch、Semaphore、BlockingQueue

相关推荐
Anastasiozzzz1 小时前
从有限状态机到智能体图:传统 FSM 与 Agent Graph的演进
java·人工智能·python·ai
wang09077 小时前
自己动手写一个spring之IOC_2
java·后端·spring
来杯@Java7 小时前
学生选课管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·maven·mybatis
不知名的老吴8 小时前
线程的生命周期之线程“插队“
java·开发语言·python
ANnianStriver8 小时前
PetLumina-02-后端开发与前后端联调
java·ai·sa-token
杨了个杨89829 小时前
Keepalived + Nginx + HAProxy 高可用架构部署实战案例
java·nginx·架构
马士兵教育11 小时前
Java还有前景吗?Java+AI大模型学习路线及项目?
java·人工智能·python·学习·机器学习
snow@li12 小时前
Java:理解 Gradle / 后端项目的管家 / 打包SpringBoot 应用 / 完成编译、下载依赖、运行测试、打包 JAR/WAR / 速查表
java
云烟成雨TD12 小时前
Spring AI 1.x 系列【57】动态工具发现:Tool Search Tool
java·人工智能·spring