【多线程】多线程打印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

相关推荐
zuoerjinshu18 小时前
【SpringBoot】讲清楚日志文件&&lombok
java·spring boot·后端
生产队队长18 小时前
SpringBoot3:ApplicationAvailability监控应用程序可用性状态[官方文档]
java·spring·mybatis
野犬寒鸦18 小时前
从零起步学习计算机操作系统:进程篇(基础知识夯实)
java·服务器·后端·学习·面试
业精于勤_荒于稀18 小时前
服务器配置
java·服务器·jvm
Barkamin19 小时前
堆排序简单实现
java·数据结构·算法·排序算法
小江的记录本19 小时前
【TCP】TCP三次握手与四次挥手(系统性知识体系+对比表格)
java·服务器·网络·网络协议·tcp/ip·http·tcp
闻哥19 小时前
MySQL索引核心原理:B+树生成、页分裂与页合并全解析
java·jvm·b树·mysql·adb·面试·springboot
蜡台19 小时前
Android Gradle 项目下载编译失败解决---持续更新
android·java·kotlin·gradle
迈巴赫车主19 小时前
天梯赛 L2-004 这是二叉搜索树吗?java
java·开发语言·数据结构·算法·天梯赛
JMchen12319 小时前
跨技术栈:在Flutter/Compose中应用自定义View思想
java·经验分享·flutter·canvas·dart·自定义view