一文搞懂四種同步工具類
_ 作者:_CoderV 的進階筆記
https://juejin.cn/post/6844903958360621064
CountDownLatch
解釋:
CountDownLatch 相當於一個門閂,門閂上掛了 N 把鎖。只有 N 把鎖都解開的話,門纔會打開。怎麼理解呢?我舉一個賽跑比賽的例子,賽跑比賽中必須等待所有選手都準備好了,裁判才能開發令槍。選手纔可以開始跑。
CountDownLatch 當中主要有兩個方法,一個是 await() 會掛上鎖阻塞當前線程,相當於裁判站在起始點等待,等待各位選手準備就緒,一個是 countDown 方法用於解鎖,相當於選手準備好了之後調用 countDown 方法告訴裁判自己準備就緒,當所有人都準備好了之後裁判開發令槍。
代碼:
public class TestCountDownLatch {
public static void main(String[] args) {
// 需要等待兩個線程,所以傳入參數爲2
CountDownLatch latch = new CountDownLatch(2);
// 該線程運行1秒
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1號選手準備就緒!用時1秒!");
latch.countDown();
}
}).start();
// 該線程運行3秒
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("2號選手準備就緒!用時3秒!");
latch.countDown();
}
}).start();
try {
System.out.println("請1號選手和2號選手各就各位!");
// 主線程在此等待兩個線程執行完畢之後繼續執行
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 兩個線程執行完畢後,主線程恢復運行
System.out.println("裁判發槍,1號選手和2號選手開跑!");
}
}
運行結果:
請1號選手和2號選手各就各位!
1號選手準備就緒!用時1秒!
2號選手準備就緒!用時3秒!
裁判發槍,1號選手和2號選手開跑!
如果去掉 CountDownLatch 的效果呢?運行結果就會變成如下:
請1號選手和2號選手各就各位!
裁判發槍,1號選手和2號選手開跑!
1號選手準備就緒!用時1秒!
2號選手準備就緒!用時3秒!
裁判就會在選手還未準備就緒的時候開發令槍,這就亂套了。
其實 CountDownLatch 一個最簡單的用處就是計算多線程執行完畢時的時間。像剛纔的例子當中兩個線程並行執行了共花費了 3 秒鐘。
CyclicBarrier
解釋:
CyclicBarrier 就像一個柵欄,將各個線程攔住。Cyclic 是循環的英文,表明該工具可以進行循環使用。CyclicBarrier(N) 的構造參數表明該一共有幾個線程需要互相等待。它相當於 N 個選手約定進行多次比賽,每次比賽完都要在起跑點互相等待。
讀者可能會馬上疑惑這不是和 CountDownLatch 一樣嗎?不一樣。因爲 CountDownLatch 是裁判等待選手,是調用 await() 方法的線程,等待調用 countDown() 方法的各個線程。而 CyclicBarrier 是選手等待選手,是調用 await() 方法的線程互相等待,等待其他線程都運行好之後,再開始下一輪運行。
我們舉一個例子,兩個選手進行比賽,一共進行三輪比賽。
代碼:
public class TestCyclicBarrier {
// 1號選手跑的輪數
public static int countA = 1;
// 2號選手跑的輪數
public static int countB = 1;
public static void main(String[] args) {
// 填入2,代表2個線程互相等待
CyclicBarrier barrier = new CyclicBarrier(2);
new Thread(new Runnable() {
@Override
public void run() {
// 一共跑三輪
for (int i = 0; i < 3; i++) {
System.out.println("1號選手開始跑!當前第" + countA++ + "輪比賽!");
// 1號選手跑得慢,每次跑三秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("1號選手抵達終點!");
// 調用等待方法,在此等待其他選手
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// 一共等待三輪
for (int i = 0; i < 3; i++) {
System.out.println("2號選手開始跑!當前第" + countB++ + "輪比賽!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("2號選手抵達終點!");
// 調用等待方法,在此等待其他選手
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
運行結果:
1號選手開始跑!當前第1輪比賽!
2號選手開始跑!當前第1輪比賽!
2號選手抵達終點!
1號選手抵達終點!
1號選手開始跑!當前第2輪比賽!
2號選手開始跑!當前第2輪比賽!
2號選手抵達終點!
1號選手抵達終點!
1號選手開始跑!當前第3輪比賽!
2號選手開始跑!當前第3輪比賽!
2號選手抵達終點!
1號選手抵達終點!
每輪比賽 1 號選手和 2 號選手都會回到起跑線互相等待,再開啓下一輪比賽。
如果不加 CyclicBarrier 呢?
1號選手開始跑!當前第1輪比賽!
2號選手開始跑!當前第1輪比賽!
2號選手抵達終點!
2號選手開始跑!當前第2輪比賽!
2號選手抵達終點!
2號選手開始跑!當前第3輪比賽!
1號選手抵達終點!
1號選手開始跑!當前第2輪比賽!
2號選手抵達終點!
1號選手抵達終點!
1號選手開始跑!當前第3輪比賽!
1號選手抵達終點!
此時 2 號選手就直接跑完三輪比賽,不等 1 號選手了。
Semaphore
Semaphore 英文的字面意思是信號量。它的工作機制是每個線程想要獲取運行的機會的話,都必須獲取到信號量。acquire() 方法阻塞的獲取信號量,release() 釋放信號量。
舉個例子,假設我們去迪士尼遊玩,但是迪士尼擔心遊客很多的話,影響大家的遊玩體驗,於是規定每個小時只能賣出兩張門票。這樣就可以控制在遊樂園當中的遊客數量了。
代碼:
public class TestSemaphore {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
System.out.println("顧客在售票處等候中");
new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
try {
Thread.sleep(500);
// 等待出票
semaphore.acquire();
System.out.println("顧客拿到門票入場!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
try {
// 等待一小時再發門票
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 一次性發出兩張門票
System.out.println("售票處第" + (i + 1) + "小時售出兩張票!");
semaphore.release();
semaphore.release();
}
}
}).start();
System.out.println("售票處開始售票!");
}
}
運行結果:
顧客在售票處等候中...
售票處開始售票!
售票處第1小時售出兩張票!
顧客拿到門票入場!
顧客拿到門票入場!
售票處第2小時售出兩張票!
顧客拿到門票入場!
顧客拿到門票入場!
售票處第3小時售出兩張票!
顧客拿到門票入場!
顧客拿到門票入場!
Exchanger
解釋:
Exchanger 提供了讓兩個線程互相交換數據的同步點。Exchanger 有點像 2 個線程的 CyclicBarrier,線程之間都是互相等待,區別在於 Exchanger 多了交換的操作。
舉個例子好比以前玩網遊的時候,買家和賣家必須走到地圖上同一地點面對面進行交易一樣,一手交錢一手交裝備。
代碼:
public class TestExchanger {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(new Runnable() {
@Override
public void run() {
String weapon = "裝備";
System.out.println("我是賣家,我帶着" + weapon + "過來了!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("賣家到達地圖上交易地點");
try {
System.out.println("我是賣家,換回了" + exchanger.exchange(weapon));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String money = "一萬遊戲幣";
System.out.println("我是買家,我帶着" + money + "過來了");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("買家到達地圖上交易地點");
try {
System.out.println("我是買家,換回了" + exchanger.exchange(money));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
運行結果:
我是賣家,我帶着裝備過來了!
我是買家,我帶着一萬遊戲幣過來了
賣家達到交易地點
買家到達交易地點
我是買家,換回了裝備
我是賣家,換回了一萬遊戲幣
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/j2EtFf_hOJm9L_y2jje7uQ