第一篇文章中,我用如何保證線程順序執行的例子作為Java並發系列的開胃菜。本篇我們依然不會有源碼分析,而是用另外兩個多線程的例子來引出Java.util.concurrent
中的幾個並發工具的用法。
系列文章
Java並發編程(一)如何保證線程順序執行 - 簡書 (jianshu.com)
一、如何保證多個線程同時執行
保證多個線程同時執行,指的是多個線程在同一時間開始執行內部run()
方法。
經過第一篇的學習,你應該能理解到,讓線程能按我們的意志來運行其實是需要用一些手段(信號量、並發工具、線程池等)來實現的。常用的並發工具一般有CountDownLatch、CyclicBarrier、Semaphore,這些工具在多線程編程中必不可少。我們先看看如何用並發工具保證線程同時執行吧。
1. 使用CountDownLatch實現
關於CountDownLatch
,count down的字面意思是倒數,latch是上鎖的意思。所以CountDownLatch的意思就是倒數關門。我們看看JDK8 API中是如何解釋的:
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
大概意思是,CountDownLatch
是一種同步輔助工具,允許一個或多個線程等待一組在其他線程中執行的操作完成之后再執行。
public class SimultaneouslyExample {
static CountDownLatch countDownLatch=new CountDownLatch(3);
public static void foo(String name) {
System.out.println("線程名:"+name+",開始時間:"+System.nanoTime());
try {
countDownLatch.await();
//2.每次減一
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(() -> foo("A"));
Thread thread2 = new Thread(() -> foo("B"));
Thread thread3 = new Thread(() -> foo("C"));
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(300);
countDownLatch.countDown();
}
}
輸出結果:
線程名:A,開始時間:449768159780400
線程名:C,開始時間:449768159785200
線程名:B,開始時間:449768159795300
看到輸出結果,你可能會懷疑。明明A線程慢了4800納秒啊,這不是同步的。其實大可不必覺得奇怪,納秒級的時間即使是JVM也沒辦法那么精准的把控,不過根據我的測試。這里的同步實現邏輯能保證毫秒級的精確性。
2. 使用CyclicBarrier實現
另一種實現方式CyclicBarrier
,根據字面意思我可以看到這個是一個可循環屏障。CyclicBarrier
可以讓一個或多個線程到達一個屏障點之后再開始運行。
話不多說,我們直接看看代碼中如何寫:
public class CyclicBarrierExample{
private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
public static void foo(String name) {
System.out.println("線程名:"+name+",開始時間:"+System.currentTimeMillis());
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException{
Thread thread1 = new Thread(() -> foo("A"));
Thread thread2 = new Thread(() -> foo("B"));
Thread thread3 = new Thread(() -> foo("C"));
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(300);
}
}
輸出結果:
線程名:A,開始時間:1621232496385
線程名:B,開始時間:1621232496385
線程名:C,開始時間:1621232496385
二、如何保證多個線程交替執行
保證多個線程交替執行,指的是多個線程可以按照一定的次序開始執行內部run()
方法。這里我們需要使用Semaphore
並發工具來實現。如何你的大學課程學習過操作系統的話,那么你一定對信號量機制很熟悉
Semaphore(信號量):是一種計數器,用來保護一個或者多個共享資源的訪問。如果線程要訪問一個資源就必須先獲得信號量。如果信號量內部計數器大於0,信號量減1,然后允許共享這個資源;否則,如果信號量的計數器等於0,信號量將會把線程置入休眠直至計數器大於0.當信號量使用完時,必須釋放。
Semaphore
的初始化需要傳入一個整型參數,此參數標識該信號量可以占用的資源個數。例如我們有兩個信號量A,B。A信號量可以允許兩個線程占用,B信號量允許一個線程占用,那么初始化的時候Semaphore A = new Semaphore(2);
public class AlternateExample {
private static Semaphore s1 = new Semaphore(1);
private static Semaphore s2 = new Semaphore(1);
private static Semaphore s3 = new Semaphore(1);
static Semaphore[] signals = {s1, s2, s3};
public static void foo(int name) {
while (true) {
try {
signals[name - 1].acquire();
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程名:" + name);
signals[(name) % 3].release();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> foo(1));
Thread thread2 = new Thread(() -> foo(2));
Thread thread3 = new Thread(() -> foo(3));
//先占用1和2,此處我們要保證的順序是3、1、2
s1.acquire();
s2.acquire();
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(300);
}
}
三、總結
本篇我們用兩個問題引出了3個並發工具CountDownLatch
、CyclicBarrier
、Semaphore
的實際應用的例子。下一篇我們講從源碼角度詳細分析下這三個工具的實現細節。
參考文章
【完整代碼】使用Semaphore實現線程的交替執行打印 A1B2C3D4E5_學亮編程手記-CSDN博客
CountDownLatch詳解 - 簡書 (jianshu.com)
Java中多個線程交替循環執行 - 坐看雲起時_雨宣 - 博客園 (cnblogs.com)
JAVA Semaphore詳解 - 簡單愛_wxg - 博客園 (cnblogs.com)