(一)题目
三个线程,第一个线程打印 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开销;
几种误解:
- 外部加锁 ≠ 一直霸占锁,await 的真实行为不是占有锁睡眠,而是释放锁进入等待队列,此时其他线程可以获取锁。
- 不会导致死锁:不存在"持有锁并等待"的情况;
- 不会导致其他线程饥饿:饥饿的前提是有线程长期无法获得锁,但是在当前实现中,每个线程打印一次必然
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。虚假唤醒产生的原因?
- 操作系统层面:在内核实现里,一次性唤醒多个线程。多出来的线程就属于"虚假唤醒"。线程在内核中可能因为时钟中断、CPU 抢占被强制唤醒或重新调度。
- JVM 层面:JVM 在实现 Condition 时可能合并等待队列、重排序唤醒逻辑等,不保证 signal 的精确性。
信号丢失产生的原因?信号丢失发生在 await() 内部的 释放锁但尚未进入 Condition 等待队列 的窗口期(这两个操作在 await 内部不是原子化的),此时其他线程可以获得锁并调用 signal(),但由于等待队列为空,信号被丢弃。
while 并不能解决信号丢失,信号该丢还是会丢。它解决的是"即使信号丢失,程序也不会因此进入错误状态",唤醒之后会再次判断,满足条件才继续执行。而不是像 if,唤醒就直接往下走。
可以解决信号丢失的方案:
要彻底避免信号丢失,唯一的方法是:不用"瞬时信号",而用"可累计 / 可回放的状态"。
比如:CountDownLatch、Semaphore、BlockingQueue