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

相关推荐
乡野码圣2 小时前
【RK3588 Android12】RCU机制
java·jvm·数据库
JAVA+C语言2 小时前
如何优化 Java 多主机通信的性能?
java·开发语言·php
编程彩机3 小时前
互联网大厂Java面试:从分布式架构到大数据场景解析
java·大数据·微服务·spark·kafka·分布式事务·分布式架构
小酒窝.4 小时前
【多线程】多线程打印1~100
java·多线程
君爱学习4 小时前
基于SpringBoot的选课调查系统
java
APIshop4 小时前
Java 实战:调用 item_search_tmall 按关键词搜索天猫商品
java·开发语言·数据库
血小板要健康4 小时前
Java基础常见面试题复习合集1
java·开发语言·经验分享·笔记·面试·学习方法
淼淼7634 小时前
安装jdk1.8
java·开发语言
毕设源码-朱学姐5 小时前
【开题答辩全过程】以 高校食堂餐饮管理系统的设计与实现为例,包含答辩的问题和答案
java