大家好,我是老三,這篇文章分享一道非常不錯的題目:三個線程按序列印ABC。
很多讀者朋友應該都覺得這道題目不難,這次給大家帶來十二種做法,一定有你沒有見過的新姿勢。
1. synchronized+wait+notify
說到同步,我們很容易就想到synchronized。
線程間通信呢?我們先回憶一下線程間的排程。

可以看到,等待和運作之間的轉換可以用wait和notify。
那麼整體思路也就有了:
- 列印的時候需要擷取鎖
- 列印B的線程需要等待列印A線程執行完,列印C的線程需要等待列印B線程執行完
- 代碼
public class ABC1 {
//鎖住的對象
private final static Object lock = new Object();
//A是否已經執行
private static boolean aExecuted = false;
//B是否已經執行過
private static boolean bExecuted = false;
public static void printA() {
synchronized (lock) {
System.out.println("A");
aExecuted = true;
//喚醒所有等待線程
lock.notifyAll();
}
}
public static void printB() throws InterruptedException {
synchronized (lock) {
//擷取到鎖,但是要等A執行
while (!aExecuted) {
lock.wait();
}
System.out.println("B");
bExecuted = true;
lock.notifyAll();
}
}
public static void printC() throws InterruptedException {
synchronized (lock) {
//擷取到鎖,但是要等B執行
while (!bExecuted) {
lock.wait();
}
System.out.println("C");
}
}
}
- 測試:後面幾種方法的單測基本和這種方法一緻,是以後面的單測就省略了。
@Test
void abc1() {
//線程A
new Thread(() -> {
ABC1.printA();
}, "A").start();
//線程B
new Thread(() -> {
try {
ABC1.printB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "B").start();
//線程C
new Thread(() -> {
try {
ABC1.printC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "C").start();
}
2. lock+全局變量state
還可以用lock+state來實作,大概思路:
- 用lock來實作同步
- 用全局變量state辨別改哪個線程執行,不執行就釋放鎖
- 代碼
public class ABC2 {
//可重入鎖
private final static Lock lock = new ReentrantLock();
//判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
private static int state = 1;
public static void printA() {
//自旋
while (state < 4) {
try {
//擷取鎖
lock.lock();
//并發情況下,不能用if,要用循環判斷等待條件,避免虛假喚醒
while (state == 1) {
System.out.println("A");
state++;
}
} finally {
//要保證不執行的時候,鎖能釋放掉
lock.unlock();
}
}
}
public static void printB() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
//擷取到鎖,應該執行
while (state == 2) {
System.out.println("B");
state++;
}
} finally {
lock.unlock();
}
}
}
public static void printC() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
while (state == 3) {
//擷取到鎖,應該執行
System.out.println("C");
state++;
}
} finally {
lock.unlock();
}
}
}
}
這裡也有幾個細節要注意:
- 要在循環裡擷取鎖,不然線程可能會在擷取到鎖之前就終止了
- 要用while,而不是if判斷,是否目前線程應該列印輸出
- 要在finally裡釋放鎖,保證其它的線程能擷取到鎖
3. volatile
上一種做法,我們用了同步+全局變量的方式,那麼有沒有什麼更輕量級的做法?
我們可以直接用volatile修飾變量,volatile能保證變量的更改對所有線程可見。
- 代碼
public class ABC3 {
//判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
private static volatile Integer state = 1;
public static void printA() {
//通過循環,hang住線程
while (state != 1) {
}
System.out.println("A");
state++;
}
public static void printB() throws InterruptedException {
while (state != 2) {
}
System.out.println("B");
state++;
}
public static void printC() throws InterruptedException {
while (state != 3) {
}
System.out.println("C");
state++;
}
}
4. AtomicInteger
除了無鎖的volatile方法,還有沒有什麼輕量級鎖的方法呢?
我們都知道synchronized和lock都屬于悲觀鎖,我們還可以用樂觀鎖來實作。
在Java裡,我們熟悉的原子操作類AtomicInteger就是基于CAS實作的,可以用來保證Integer操作的原子性。
- 代碼
public class ABC4 {
//判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
private static AtomicInteger state = new AtomicInteger(1);
public static void printA() {
System.out.println("A");
state.incrementAndGet();
}
public static void printB() throws InterruptedException {
while (state.get() < 4) {
while (state.get() == 2) {
System.out.println("B");
state.incrementAndGet();
}
}
}
public static void printC() throws InterruptedException {
while (state.get() < 4) {
while (state.get() == 3) {
System.out.println("C");
state.incrementAndGet();
}
}
}
}
5.lock+condition
在Java中,除了Object的wait和notify/notify可以實作等待/通知機制,Condition和Lock配合同樣可以完成等待通知機制。
使用
condition.await()
,使目前線程進入等待狀态,使用
condition.signal()
或者
condition.signalAll()
喚醒等待線程。
- 代碼
public class ABC5 {
//可重入鎖
private final static Lock lock = new ReentrantLock();
//判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
private static int state = 1;
//condition對象
private static Condition a = lock.newCondition();
private static Condition b = lock.newCondition();
private static Condition c = lock.newCondition();
public static void printA() {
//通過循環,hang住線程
while (state < 4) {
try {
//擷取鎖
lock.lock();
//并發情況下,不能用if,要用循環判斷等待條件,避免虛假喚醒
while (state != 1) {
a.await();
}
System.out.println("A");
state++;
b.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//要保證不執行的時候,鎖能釋放掉
lock.unlock();
}
}
}
public static void printB() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
//擷取到鎖,應該執行
while (state != 2) {
b.await();
}
System.out.println("B");
state++;
c.signal();
} finally {
lock.unlock();
}
}
}
public static void printC() throws InterruptedException {
while (state < 4) {
try {
lock.lock();
while (state != 3) {
c.await();
}
//擷取到鎖,應該執行
System.out.println("C");
state++;
} finally {
lock.unlock();
}
}
}
}
6.信号量Semaphore
線程間同步,還可以使用信号量Semaphore,信号量顧名思義,多線程協作時完成信号傳遞。
使用acquire()擷取許可,如果沒有可用的許可,線程進入阻塞等待狀态;使用release釋放許可。
- 代碼
public class ABC6 {
private static Semaphore semaphoreB = new Semaphore(0);
private static Semaphore semaphoreC = new Semaphore(0);
public static void printA() {
System.out.println("A");
semaphoreB.release();
}
public static void printB() throws InterruptedException {
semaphoreB.acquire();
System.out.println("B");
semaphoreC.release();
}
public static void printC() throws InterruptedException {
semaphoreC.acquire();
System.out.println("C");
}
}
7.計數器CountDownLatch
CountDownLatch的一個适用場景,就是用來進行多個線程的同步管理,線程調用了countDownLatch.await()之後,需要等待countDownLatch的信号countDownLatch.countDown(),在收到信号前,它不會往下執行。
public class ABC7 {
private static CountDownLatch countDownLatchB = new CountDownLatch(1);
private static CountDownLatch countDownLatchC = new CountDownLatch(1);
public static void printA() {
System.out.println("A");
countDownLatchB.countDown();
}
public static void printB() throws InterruptedException {
countDownLatchB.await();
System.out.println("B");
countDownLatchC.countDown();
}
public static void printC() throws InterruptedException {
countDownLatchC.await();
System.out.println("C");
}
}
8. 循環栅欄CyclicBarrier
用到了
CountDownLatch
,我們應該想到,還有一個功能和它類似的工具類
CyclicBarrier
。
有的翻譯叫同步屏障,我覺得翻譯成循環栅欄,更能展現它的功能特性。
就像是出去旅遊,大家不同時間到了景區門口,但是景區疫情限流,先把栅欄拉下來,在景區裡的遊客走一批,打開栅欄,再放進去一批,走一批,再放進去一批……
這就是CyclicBarrier的兩個特性,
- 栅欄:多個線程互相等待,到齊後再執行特定動作
- 循環:所有線程釋放後,還能繼續複用它
這道題怎麼用CyclicBarrier解決呢?
- 線程B和線程C需要使用栅欄等待
- 為了讓B和C也順序執行,需要用一個狀态,來辨別應該執行的線程
- 代碼
public class ABC8 {
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(1);
private static Integer state = 1;
public static void printA() {
while (state != 1) {
}
System.out.println("A");
state = 2;
}
public static void printB() throws InterruptedException {
try {
//在栅欄前等待
cyclicBarrier.await();
//state不等于2的時候等待
while (state != 2) {
}
System.out.println("B");
state = 3;
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
public static void printC() throws InterruptedException {
try {
cyclicBarrier.await();
while (state != 3) {
}
System.out.println("C");
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
當然,CyclicBarrier的實作其實還是基于lock+condition,多個線程在到達一定條件前await,到達條件後signalAll。
9.交換器Exchanger
在前面,我們已經用到了常用的并發工具類,其實還有一個不那麼常用的并發工具類
Exchanger
,同樣也可以用來解決這道題目。
Exchanger用于兩個線程在某個節點時進行資料交換,在這道題裡:
- 線程A執行完之後,和線程B用一個交換器交換state,線程B執行完之後,和線程C用一個交換器交換state
- 在沒有輪到自己執行之前,先進行等待
public class ABC9 {
private static Exchanger<Integer> exchangerB = new Exchanger<>();
private static Exchanger<Integer> exchangerC = new Exchanger<>();
public static void printA() {
System.out.println("A");
try {
//交換
exchangerB.exchange(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void printB() {
try {
//交換
Integer state = exchangerB.exchange(0);
//等待
while (state != 2) {
}
//執行
System.out.println("B");
//第二次交換
exchangerC.exchange(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void printC() {
try {
Integer state = exchangerC.exchange(0);
while (state != 3) {
}
System.out.println("C");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Exchanger是基于ThreadLocal實作的,那麼我們這個問題可以基于ThreadLocal來實作嗎?
10.ThreadLocal
ThreadLocal,我們應該都了解過它的用法和原理,那麼怎麼用ThreadLocal實作三個線程順序列印ABC呢?
子線程是并發執行的,但是主線程的代碼是順序執行的,我們在主線程裡改變變量,子線程根據變量判斷。
那麼問題來了,子線程怎麼擷取主線程的變量呢?可以用
InheritableThreadLocal
。
- 代碼
public class ABC10 {
public static void main(String[] args) {
//使用ThreadLocal存儲變量
ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
threadLocal.set(1);
new Thread(() -> {
System.out.println("A");
}, "A").start();
//設定變量值
threadLocal.set(2);
new Thread(() -> {
//等待
while (threadLocal.get() != 2) {
}
System.out.println("B");
}, "B").start();
threadLocal.set(3);
new Thread(() -> {
while (threadLocal.get() != 3) {
}
System.out.println("C");
}, "C").start();
}
}
11.管道流PipedStream
線程之間通信,還有一種比較笨重的辦法——PipedInputStream/PipedOutStream。
一個線程使用PipedOutStream寫資料,一個線程使用PipedInputStream讀資料,而且Piped的讀取隻能一對一。
那麼,在這道題裡:
- 線程A使用PipedOutStream向線程B寫入資料,線程B讀取後,列印輸出
- 線程B和C也是相同的姿勢
- 代碼
public class ABC11 {
public static void main(String[] args) throws IOException {
//線程A的輸出流
PipedOutputStream outputStreamA = new PipedOutputStream();
//線程B的輸出流
PipedOutputStream outputStreamB = new PipedOutputStream();
//線程B的輸入流
PipedInputStream inputStreamB = new PipedInputStream();
//線程C的輸入流
PipedInputStream inputStreamC = new PipedInputStream();
outputStreamA.connect(inputStreamB);
outputStreamB.connect(inputStreamC);
new Thread(() -> {
System.out.println("A");
try {
//流寫入
outputStreamA.write("B".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}, "A").start();
new Thread(() -> {
//流讀取
byte[] buffer = new byte[1];
try {
inputStreamB.read(buffer);
//轉換成String
String msg = new String(buffer);
System.out.println(msg);
outputStreamB.write("C".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}, "B").start();
new Thread(() -> {
byte[] buffer = new byte[1];
try {
inputStreamC.read(buffer);
String msg = new String(buffer);
System.out.println(msg);
} catch (IOException e) {
e.printStackTrace();
}
}, "C").start();
}
}
12.阻塞隊列BlockingQueue
阻塞隊列同樣也可以用來進行線程排程。
- 利用隊列的長度,來确定執行者
- 利用隊列的阻塞性,來保證入隊操作同步執行。
- 代碼
public class ABC12 {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
public static void printA() {
System.out.println("A");
queue.offer("B");
}
public static void printB() throws InterruptedException {
while (queue.size() != 1) {
}
System.out.println("B");
queue.offer("C");
}
public static void printC() throws InterruptedException {
while (queue.size() != 2) {
}
System.out.println("C");
}
}
總結
這篇文章給大家帶來了
三個線程順序列印ABC的
的十二種做法,裡面有些寫法肯定是備援的,大家有沒有什麼更好的寫法呢?
通過十二種題解,我們基本上把Java并發中主要的線程同步和通信方式過了一遍,相信通過這道題的實踐,我們也能對Java線程的同步和通信有更深的了解。
最後,也給大家留兩道“進階”一點的題目,感興趣可以自己實作一下:
- 兩個線程,一個線程列印奇數,一個線程列印偶數
- 按照順序,三個線程分别列印A5次,B10次,C15次
參考:
[1]. https://zhuanlan.zhihu.com/p/368409843