Java多線程有Runnable、Thread、Callable、線程池、synchronized、volatile、Lock等可以直接使用。也有線程的直接實現可用。
下邊主要講下CountDownLatch、CyclicBarrier、Semaphore與Exchanger
CountDownLatch
從名字可以知道,是個倒計數鎖。通過一個計數器,每個線程完成則減一,並在原地等待。直至減到0,開始後續工作。
應用場景:應用場景:A、B、C三個任務,可以併發執行,然後都執行完後纔可以執行任務D。
public class TryCountDownLatch implements Runnable {
private int sequence;
public TryCountDownLatch(int sequence) {
this.sequence = sequence;
}
// 初始化計數器,注意這裏是 static 的。爲了共用
static final CountDownLatch latch = new CountDownLatch(10);
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println("Complete Run " + sequence);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 計數減一
latch.countDown();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
exec.submit(new TryCountDownLatch(i));
}
// 等待檢查
System.out.println("All Thread Wait " + System.currentTimeMillis());
latch.await();
System.out.println("All Thread Completed " + System.currentTimeMillis());
// 關閉線程池
exec.shutdown();
}
}
這裏主線程會阻塞在 await() 的地方,然後所有線程類執行後都調用CountDownLatch的countDown()方法,即數字減一。直到爲0,主線程開始繼續工作。幫我們解決了多線程的執行依賴關係。
CyclicBarrier
也即是我們常說的柵欄類,線程走到柵欄後阻塞等待,直到所有線程都滿足才能繼續往下執行。至於它與CountDownLatch 的區別,網上說CyclicBarrier是N個線程相互等待,而
CyclicBarrier 是一個後續線程等待N個線程。我覺得沒事區別。唯一的區別是:CountDownLatch採用計算器,只能使用一次。而CyclicBarrier 即循環柵欄,也就是說它可以循環使用。
它的用法跟CountDownLatch 差不多,首先它的構造函數需要一個等待線程數,和一個後續線程任務
public class TestCyclicBarrier {
public static void main(String[] args) {
// 注意這裏柵欄數和下邊的線程數
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
System.out.println("所有線程均完成,此處進行柵欄後的收尾工作");
}
});
for (int i = 0; i < 5; i++) {
TryCyclicBarrier thread = new TryCyclicBarrier(barrier, i);
new Thread(thread).start();
}
System.out.println("主線程結束 " + System.currentTimeMillis());
}
}
在所有要先執行的線程裏調用await() 方法
public class TryCyclicBarrier implements Runnable {
private CyclicBarrier cyclicBarrier;
private int sequence;
public TryCyclicBarrier(CyclicBarrier cyclicBarrier, int sequence) {
this.cyclicBarrier = cyclicBarrier;
this.sequence = sequence;
}
@Override
public void run() {
try {
System.out.println("線程 " + sequence + " 開始工作");
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println("線程 " + sequence + " 到達柵欄");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
在有指定線程數 await() 後,等待的線程任務纔開始執行。輸出如下:
線程 2 開始工作
線程 1 開始工作
線程 4 開始工作
線程 3 開始工作
線程 0 開始工作
主線程結束 1585920830794
線程 3 到達柵欄
線程 0 到達柵欄
線程 4 到達柵欄
線程 1 到達柵欄
線程 2 到達柵欄
所有線程均完成,此處進行柵欄後的收尾工作
注意:如果超過指定線程數,等待的線程任務有可能會再次執行。
假如CyclicBarrier 指定了需要3個線程await()和一個後續線程任務D,當A、B、C三個await後,CyclicBarrier會執行D。之後A、B、C三個再次 await() 後,還會再次執行一次任務D。
Semaphore
即信號量類。我們知道synchronized 用來控制方法或者代碼塊互斥的,同一時間只有一個線程進入。Semaphore 是 synchronized 的加強版,作用是控制線程的併發數量。
新建一個信號量對象,並設置最多可進入的線程數。在要控制併發的代碼之前調用 acquire() ,之後調用 release() 方法。
public class TrySemaphore {
// 同步關鍵類,構造方法傳入的數字是多少,則同一個時刻,最多允許多少個進程同時運行
private Semaphore semaphore = new Semaphore(2);
public void process(String threadName) throws Exception {
// 在 semaphore.acquire() 和 semaphore.release()之間的代碼,同一時刻只允許指定個數線程進入,
semaphore.acquire();
System.out.println(System.currentTimeMillis() + " 進入互斥區 " + threadName);
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println(System.currentTimeMillis() + " 離開互斥區 " + threadName);
semaphore.release();
}
}
然後將該任務放入多線程中執行:
public class TestSemaphore extends Thread {
private TrySemaphore work;
private int sequence;
public TestSemaphore(TrySemaphore work, int sequence) {
this.work = work;
this.sequence = sequence;
}
@Override
public void run() {
try {
this.work.process("Thread " + sequence);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
TrySemaphore trySemaphore = new TrySemaphore();
for (int i = 0; i < 5; i++) {
TestSemaphore thread = new TestSemaphore(trySemaphore, i);
thread.start();
}
}
}
執行結果如下:
1585920227612 進入互斥區 Thread 0
1585920227612 進入互斥區 Thread 1
1585920233615 離開互斥區 Thread 1
1585920233616 進入互斥區 Thread 2
1585920235616 離開互斥區 Thread 0
1585920235616 進入互斥區 Thread 3
1585920239618 離開互斥區 Thread 2
1585920239618 進入互斥區 Thread 4
1585920242619 離開互斥區 Thread 3
1585920242622 離開互斥區 Thread 4
從上邊日誌輸出可以看出,最開始只有倆線程進入互斥區,然後有線程離開後,其他線程才能進去該代碼區。從這點來說,它跟synchronized 效果一模一樣,只是允許的線程數量大於1而已。
Exchanger
Exchanger 是一個交換服務,允許原子性的交換兩個(多個)對象,但同時只有一對纔會成功。
當一個線程到達 exchange 調用點時,如果其他線程此前已經調用了此方法,則其他線程會被調度喚醒並與之進行對象交換,然後各自返回;
如果其他線程還沒到達交換點,則當前線程會被掛起,直至其他線程到達纔會完成交換並正常返回,或者當前線程被中斷或超時返回
例如
public class TestExchange {
static class Processer extends Thread {
private Exchanger<String> exchanger;
public Processer(String name, Exchanger<String> exchanger) {
super(name);
this.exchanger = exchanger;
}
@Override
public void run() {
for (int i = 1; i < 5; i++) {// 注意:這裏從1 開始,每個線程去去交換4次
try {
TimeUnit.SECONDS.sleep(1);
String preData = "From" + getName() + " data" + i;
String postData = exchanger.exchange(preData);
System.out.println(getName() + " 交換前:" + preData + " 交換後:" + postData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<String>();
new Processer("Processer-1", exchanger).start();
new Processer("Processer-2", exchanger).start();
new Processer("Processer-3", exchanger).start();
// TODO 3個線程 產生 4 * 3 = 12 次交換。因爲兩兩直接才能交換。所以能結束.
// 如果 這裏產生了奇數個交換,則某個線程將用於處於等等狀態
TimeUnit.SECONDS.sleep(7);
}
}
上邊正好構成偶數個交換,兩兩成功。因此可以結束。
Lock
Lock經常用來跟synchronized 比較:synchronized 可以加在類、方法、代碼塊上,報錯後自動釋放鎖,可以防止JVM對代碼重排序。而Lock 加在代碼塊上,且需要主動釋放,是Java的類。主要用的是ReentrantLock,即可重入鎖。ReadWriteLock 讀寫鎖。
對比上邊代碼,改成Lock方式:
public class TryLoack {
private int fromValue;
private int toValue;
public TryLoack(int fromValue, int toValue) {
this.fromValue = fromValue;
this.toValue = toValue;
}
public int balance(int offset) {
Lock lock = new ReentrantLock();
try {
lock.lock();
fromValue -= offset;
toValue += offset;
return fromValue + toValue;
} finally {
lock.unlock();
}
}
}
volatile
嚴格來說volatile 不能解決多線程併發互斥問題。它只涉及變量在線程中的可見行。
假設變量 var 被A、B兩個線程使用,當A使用並修改var 時,它修改的只是var 在當前線程中的副本,對此線程B是不可見的,直到本次修改被定期同步到主內存。爲了讓 A 對 var 的修改立刻被 B 感知,就需要對 var 加 volatile 修飾符。
單例設計模式中,volatile 與 synchronized 一起使用,雙重檢查來生成單例對象。參考單例模式雙重檢查