本文博主給大家講解一道網上非常經典的多線程面試題目。關於三個線程如何交替列印ABC迴圈100次的問題。 > 下文實現代碼都基於Java代碼在單個JVM內實現。 ## 問題描述 給定三個線程,分別命名為A、B、C,要求這三個線程按照順序交替列印ABC,每個字母列印100次,最終輸出結果為: ``` A ...
本文博主給大家講解一道網上非常經典的多線程面試題目。關於三個線程如何交替列印ABC迴圈100次的問題。
下文實現代碼都基於Java代碼在單個JVM內實現。
問題描述
給定三個線程,分別命名為A、B、C,要求這三個線程按照順序交替列印ABC,每個字母列印100次,最終輸出結果為:
A
B
C
A
B
C
...
A
B
C
推薦博主開源的 H5 商城項目waynboot-mall,這是一套全部開源的微商城項目,包含三個項目:運營後臺、H5 商城前臺和服務端介面。實現了商城所需的首頁展示、商品分類、商品詳情、商品 sku、分詞搜索、購物車、結算下單、支付寶/微信支付、收單評論以及完善的後臺管理等一系列功能。 技術上基於最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中間件。分模塊設計、簡潔易維護,歡迎大家點個 star、關註博主。
github 地址:https://github.com/wayn111/waynboot-mall
解決思路
這是一個典型的多線程同步的問題,需要保證每個線程在列印字母之前,能夠判斷是否輪到自己執行,以及在列印字母之後,能夠通知下一個線程執行。為了實現這一目標,博主講介紹以下5種方法:
- 使用synchronized和wait/notify
- 使用ReentrantLock和Condition
- 使用Semaphore
- 使用AtomicInteger和CAS
- 使用CyclicBarrier
方法一:使用synchronized和wait/notify
synchronized是Java中的一個關鍵字,用於實現對共用資源的互斥訪問。wait和notify是Object類中的兩個方法,用於實現線程間的通信。wait方法會讓當前線程釋放鎖,併進入等待狀態,直到被其他線程喚醒。notify方法會喚醒一個在同一個鎖上等待的線程。
我們可以使用一個共用變數state來表示當前應該列印哪個字母,初始值為0。當state為0時,表示輪到A線程列印;當state為1時,表示輪到B線程列印;當state為2時,表示輪到C線程列印。每個線程在列印完字母後,需要將state加1,並對3取模,以便迴圈。同時,每個線程還需要喚醒下一個線程,並讓自己進入等待狀態。
具體的代碼實現如下:
public class PrintABC {
// 共用變數,表示當前應該列印哪個字母
private static int state = 0;
// 共用對象,作為鎖和通信的媒介
private static final Object lock = new Object();
public static void main(String[] args) {
// 創建三個線程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 迴圈100次
for (int i = 0; i < 100; i++) {
// 獲取鎖
synchronized (lock) {
// 判斷是否輪到自己執行
while (state % 3 != 0) {
// 不是則等待
lock.wait();
}
// 列印字母
System.out.println("A");
// 修改狀態
state++;
// 喚醒下一個線程
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
while (state % 3 != 1) {
lock.wait();
}
System.out.println("B");
state++;
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
while (state % 3 != 2) {
lock.wait();
}
System.out.println("C");
state++;
lock.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 啟動三個線程
threadA.start();
threadB.start();
threadC.start();
}
}
方法二:使用ReentrantLock和Condition
ReentrantLock是Java中的一個類,用於實現可重入的互斥鎖。Condition是ReentrantLock中的一個介面,用於實現線程間的條件等待和喚醒。ReentrantLock可以創建多個Condition對象,每個Condition對象可以綁定一個或多個線程,實現對不同線程的精確控制。
我們可以使用一個ReentrantLock對象作為鎖,同時創建三個Condition對象,分別綁定A、B、C三個線程。每個線程在列印字母之前,需要調用對應的Condition對象的await方法,等待被喚醒。每個線程在列印字母之後,需要調用下一個Condition對象的signal方法,喚醒下一個線程。
具體的代碼實現如下:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class PrintABC {
// 共用變數,表示當前應該列印哪個字母
private static int state = 0;
// 可重入鎖
private static final ReentrantLock lock = new ReentrantLock();
// 三個條件對象,分別綁定A、B、C三個線程
private static final Condition A = lock.newCondition();
private static final Condition B = lock.newCondition();
private static final Condition C = lock.newCondition();
public static void main(String[] args) {
// 創建三個線程
Thread threaA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 迴圈100次
for (int i = 0; i < 100; i++) {
// 獲取鎖
lock.lock();
try {
// 判斷是否輪到自己執行
while (state % 3 != 0) {
// 不是則等待
A.await();
}
// 列印字母
System.out.println("A");
// 修改狀態
state++;
// 喚醒下一個線程
B.signal();
} finally {
// 釋放鎖
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threaB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (state % 3 != 1) {
B.await();
}
System.out.println("B");
state++;
C.signal();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threaC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
while (state % 3 != 2) {
C.await();
}
System.out.println("C");
state++;
A.signal();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 啟動三個線程
threaA.start();
threaB.start();
threaC.start();
}
}
方法三:使用Semaphore
Semaphore是Java中的一個類,用於實現信號量機制。信號量是一種計數器,用於控制對共用資源的訪問。Semaphore可以創建多個信號量對象,每個信號量對象可以綁定一個或多個線程,實現對不同線程的精確控制。
我們可以使用三個Semaphore對象,分別初始化為1、0、0,表示A、B、C三個線程的初始許可數。每個線程在列印字母之前,需要調用對應的Semaphore對象的acquire方法,獲取許可。每個線程在列印字母之後,需要調用下一個Semaphore對象的release方法,釋放許可。
具體的代碼實現如下:
import java.util.concurrent.Semaphore;
public class PrintABC {
private static int state = 0;
// 三個信號量對象,分別表示A、B、C三個線程的初始許可數
private static final Semaphore A = new Semaphore(1);
private static final Semaphore B = new Semaphore(0);
private static final Semaphore C = new Semaphore(0);
public static void main(String[] args) {
// 創建三個線程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 迴圈100次
for (int i = 0; i < 100; i++) {
// 獲取許可
A.acquire();
// 列印字母
System.out.println("A");
// 修改狀態
state++;
// 釋放許可
B.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
B.acquire();
System.out.println("B");
state++;
C.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 100; i++) {
C.acquire();
System.out.println("C");
state++;
A.release();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 啟動三個線程
threadA.start();
threadB.start();
threadC.start();
}
}
方法四:使用AtomicInteger和CAS
AtomicInteger是Java中的一個類,用於實現原子性的整數操作。CAS是一種無鎖的演算法,全稱為Compare And Swap,即比較並交換。CAS操作需要三個參數:一個記憶體地址,一個期望值,一個新值。如果記憶體地址的值與期望值相等,就將其更新為新值,否則不做任何操作。
我們可以使用一個AtomicInteger對象來表示當前應該列印哪個字母,初始值為0。當state為0時,表示輪到A線程列印;當state為1時,表示輪到B線程列印;當state為2時,表示輪到C線程列印。每個線程在列印完字母後,需要使用CAS操作將state加1,並對3取模,以便迴圈。
具體的代碼實現如下:
import java.util.concurrent.atomic.AtomicInteger;
public class PrintABC {
// 共用變數,表示當前應該列印哪個字母
private static AtomicInteger state = new AtomicInteger(0);
public static void main(String[] args) {
// 創建三個線程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
// 迴圈100次
for (int i = 0; i < 100; ) {
// 判斷是否輪到自己執行
if (state.get() % 3 == 0) {
// 列印字母
System.out.println("A");
// 修改狀態,使用CAS操作保證原子性
state.compareAndSet(state.get(), state.get() + 1);
// 計數器加1
i++;
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; ) {
if (state.get() % 3 == 1) {
System.out.println("B");
state.compareAndSet(state.get(), state.get() + 1);
i++;
}
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; ) {
if (state.get() % 3 == 2) {
System.out.println("C");
state.compareAndSet(state.get(), state.get() + 1);
i++;
}
}
}
});
// 啟動三個線程
threadA.start();
threadB.start();
threadC.start();
}
}
方法五:使用CyclicBarrier
CyclicBarrier是Java中的一個類,用於實現多個線程之間的屏障。CyclicBarrier可以創建一個屏障對象,指定一個參與等待線程數和一個到達屏障點時得動作。當所有線程都到達屏障點時,會執行屏障動作,然後繼續執行各自的任務。CyclicBarrier可以重覆使用,即當所有線程都通過一次屏障後,可以再次等待所有線程到達下一次屏障。
我們可以使用一個CyclicBarrier對象,指定三個線程為參與等待數,以及一個列印字母的到達屏障點動作。每個線程在執行完自己的任務後,需要調用CyclicBarrier對象的await方法,等待其他線程到達屏障點。當所有線程都到達屏障點時,會執行列印字母的屏障動作,並根據state的值判斷應該列印哪個字母。然後,每個線程繼續執行自己的任務,直到迴圈結束。需要註意得就是由於列印操作在到達屏障點得動作內執行,所以三個線程得迴圈次數得乘以參與線程數量,也就是三。
具體的代碼實現如下:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class PrintABC {
// 共用變數,表示當前應該列印哪個字母
private static int state = 0;
// 參與線程數量
private static int threadNum = 3;
// 迴圈屏障,指定三個線程為屏障點,以及一個列印字母的屏障動作
private static final CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {
@Override
public void run() {
// 根據state的值判斷應該列印哪個字母
switch (state) {
case 0:
System.out.println("A");
break;
case 1:
System.out.println("B");
break;
case 2:
System.out.println("C");
break;
}
// 修改狀態
state = (state + 1) % 3;
System.out.println(state);
}
});
public static void main(String[] args) {
// 創建三個線程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 迴圈100次
for (int i = 0; i < threadNum * 100; i++) {
// 執行自己的任務
// ...
// 等待其他線程到達屏障點
barrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < threadNum * 100; i++) {
// 執行自己的任務
// ...
// 等待其他線程到達屏障點
barrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < threadNum * 100; i++) {
// 執行自己的任務
// ...
// 等待其他線程到達屏障點
barrier.await();
}
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
// 啟動三個線程
threadA.start();
threadB.start();
threadC.start();
}
}
總結
到此,本文內容已經講解完畢,以上的這五種方法都可以利用不同的工具和機制來實現多線程之間的同步和通信,從而保證按照順序交替列印ABC。這些方法各有優缺點,具體的選擇需要根據實際的場景和需求來決定。
最後本文講解代碼是在單個JVM內的實現方法,如果大家對涉及到多個JVM來實現按照順序交替列印ABC的話,可以私信博主,博主再給大家出一期文章進行講解。
關註公眾號【waynblog】每周分享技術乾貨、開源項目、實戰經驗、高效開發工具等,您的關註將是我的更新動力!