文章目錄
一、簡介
1.1、 Semaphore
信號量,見文知義,常用於併發控制中的限流作用,我理解是限定數量的共享鎖機制。該共享資源最多同時可讓n個線程訪問,超過n個線程就阻塞等待,如有資源空閒, 喚醒其他等待線程(喚醒又分公平與非公平,默認非公平)比如一條四車道大橋,每次僅能併發通過4輛汽車,而在高峯期時100輛車湧入,這次需要一個信號燈來限制車輛,每次最多放行4輛車,在車輛通過後再放行。在併發環境下,每輛車就是如一個線程,4車道大橋就如有限的資源,需要控制線程的數量,在這種業務場景下,靠鎖同步的機制(如synchronized)力有不逮,java併發包中提供Semaphore類可以幫助解決此類場景。
應用場景:
1、資源控制:控制數據庫連接數,如有多個IO操作,每個需要操作數據庫寫入,數據庫連接數有限,控制連接數據庫數量。
例:100個線程執行IO,只有10個mysql連接,最多同時可以有10個線程獲取到連接,否則會報錯無法獲取連接,這時可用信號量控制。
2、可當同步鎖使用,設置信號量通道等於1。
1.2、CountDownLatch
允許一個或多個線程等待其他線程完成操作後再執行。其內部維護一個計數器,設置初始值給state,每調用 countDown()方法一次,state數量減1,調用await()方法的線程被阻塞,需要等待state減少爲0時纔可被喚醒繼續執行。
應用場景:
1、一個任務要統計公司一星期的財務流水總額,每次需要讀取5張Excel表統計流水彙總,如何快速地統計出來?
可以使用CountDownLatch,先開始5個線程併發地分別統計每張表的流水額度總和,當5個線程統計結束,再彙總總額。
2、開發對外接口,要求響應快,而該接口內部邏輯複雜,涉及多個服務的調用,並依賴這些獨立服務響應結果進行下一步操作,這時可以考慮CountDownLatch或者 CyclicBarrier,併發調用多個服務,獲取這些結果後才進行下一步,縮短處理時間。
1.3、CyclicBarrier
循環屏障,柵欄類; CyclicBarrier 可讓多個線程相互等待,當所有線程都達到後再喚醒所有線程繼續執行。如導遊設定目標點,所有遊客到這集合,先到的遊客等待其他遊客,當所有的遊客都到了後,大家再一起出發。
CyclicBarrier與CountDownLatch區別:
1) CyclicBarrier 在應用場景中,多個線程之間相互等待,線程之間在業務上可能會更有依賴性 ; CountDownLatch是每個執行 coutDown方法的線程之間可以沒有依賴性,而是執行await方法的線程更依賴這些執行coutDown的線程。
2)CountDownLatch計數器只能使用一次,Semaphore可以多次使用,可以重置使用。
3)CyclicBarrier 可以處理更復雜的業務場景,如線程都達到屏障後,可以在構造函數中 CyclicBarrier(int parties, Runnable barrierAction) 傳入線程barrierAction,當達到屏障觸發條件時,可以比其他等待線程優先執行,處理業務。
4)CyclicBarrier還有很多方法,如查看當前到達屏障被阻塞的線程數量 getNumberWaiting()。
5)CountDownLatch 是每個線程執行完減1操作,當計數器爲0時,才喚醒等待線程,阻塞線程只有1個或者多個;CyclicBarrier是讓所有線程到達屏障處就被阻塞了,當所有線程都到達時,喚醒所有被阻塞線程繼續執行,阻塞線程有多個,線程之間是相互等待。
二、信號量Semaphore
2.1、構造方法
Semaphore(int permits) //默認非公平
//是否公平嘗試獲取許可證
Semaphore(int permits, boolean fair)
2.2、主要方法
acquire() //從信號量那去獲取一個許可,如果沒有剩餘的就被阻塞
acquire(int permits)//也可一次獲取多個許可
release() //釋放一個許可,將其返還給信號量,給其他線程使用。
release(int permits) //一次釋放多個許可
2.3、示例代碼
package Semaphore;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* @author zdd
* 2019/11/27 6:43
* Description: 信號量測試
*/
public class SemaphoreService {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private Semaphore semaphore = new Semaphore(4);
public void doSomeThing() throws InterruptedException {
//1,獲取一個許可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "--start--" +getFormatDate());
// 停頓1s
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "--end--" +getFormatDate());
//2,用完釋放一個許可,可供其他線程使用
semaphore.release();
}
public static void main(String[] args) {
SemaphoreService semaphoreService = new SemaphoreService();
for (int i = 0; i < 10; i++) {
WorkThread workThread = new WorkThread(i+"",semaphoreService);
workThread.start();
}
}
public String getFormatDate() {
return sdf.format(new Date());
}
// 工作線程類 - 靜態內部類
static class WorkThread extends Thread {
SemaphoreService semaphoreService;
//構造參數傳入線程名稱及SemaphoreService
public WorkThread(String name, SemaphoreService semaphoreService) {
this.semaphoreService = semaphoreService;
setName(name);
}
@Override
public void run() {
try {
semaphoreService.doSomeThing();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.線程最多4個同時執行
//1,設置信號量數爲4,執行結果如下圖
private Semaphore semaphore = new Semaphore(4);
2.線程依次串行執行,實現同步
//1,設置信號量數爲1,執行結果如下圖
private Semaphore semaphore = new Semaphore(1);
三、計數器CountDownLatch
3.1、主要方法
countDown() 計數器減1
await() 阻塞等待,直到計數器爲0喚醒繼續執行
await(long timeout, TimeUnit unit) 阻塞等待,在等待設定時間計時器還沒到減爲0,也不會再繼續等待了。
3.2、示例代碼1
package countDownLatch;
import javax.swing.plaf.IconUIResource;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author zdd
* 2019/11/29 7:19 下午
* Description: 班長召集士兵集合拉練案例
*/
public class CountDownLatchSoldierTest {
private static final Integer THREAD_COUNT = 10;
//等待士兵10人
private static CountDownLatch countDownLatch = new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException {
//1,開10個線程模擬士兵簽到,採用線程池創建
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
for (int i = 0; i < 10; i++) {
//2,提交10個線程執行
executorService.submit(()->{
try{
System.out.println(Thread.currentThread().getName()+ ": 士兵報告");
}finally {
//3,計數器減1
countDownLatch.countDown();
}
});
}
//3,主線程做班長,負責等待所有士兵到齊,開始拉練
countDownLatch.await();
System.out.println("班長: 集合完畢,開始5公里越野! ");
//4,關閉線程池
executorService.shutdown();
}
}
執行結果如圖:
3.3、示例代碼2
計算器未減到0,主線程持續等待。
package countDownLatch;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* zdd
* 2019/11/5 10:50
* Description:測試計數器使用
*/
public class CountDownLatchTest {
static int number = 2;
//1,設置計數器的值爲 2
static CountDownLatch countDownLatch = new CountDownLatch(number);
public static void main(String[] args) throws InterruptedException {
//2,開啓一個線程,傳入計數器
new Thread(()-> {
try {
//睡2s,模擬執行業務
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("調用countDown前, 當前count 值爲:"+countDownLatch.getCount());
//3,調用遞減 1次
countDownLatch.countDown();
System.out.println("調用countDown後, 當前count 值爲:"+countDownLatch.getCount());
}
},"worker").start();
//情況1、主線程開始執行 ,等待1s之後,如果還沒到條件,也不等了哦
// countDownLatch.await(1,TimeUnit.SECONDS);
//情況2、阻塞等待,如果計數未到0,一直阻塞等待
countDownLatch.await();
System.out.println("主線程繼續執行");
}
}
worker線程執行了一次countDown(), count值爲1,所以主線程一直阻塞等待,執行結果如下:
四、柵欄類CyclicBarrier
4.1、構造方法
CyclicBarrier(int parties) //parties爲需要等待線程的數量
//barrierAction,在所有其他線程到達後,優先執行的線程。可根據業務添加
CyclicBarrier(int parties, Runnable barrierAction)
4.2、示例代碼
package CyclicBarrier;
import java.util.Map;
import java.util.concurrent.*;
/**
* @author zdd
* 2019/11/28 9:20
* Description: 開啓4個線程分別統計sheet表,使用柵欄類實現同步統計計算,最後計算總和;此處也可用
* CountDownLatch實現
*/
public class BankAccountService {
private ConcurrentHashMap<String,Integer> concurrentHashMap = new ConcurrentHashMap<>();
// 線程數
private final static int THREAD_COUNT = 4;
private CyclicBarrier cyclicBarrier = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
Integer sumcount = 0;
//彙總每個sheet計算結果
for (Map.Entry<String, Integer> entry: concurrentHashMap.entrySet()) {
sumcount+=entry.getValue();
}
System.out.println("優先執行, 求和計算完畢,總和爲:"+ sumcount);
}
});
public void count() {
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < THREAD_COUNT; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"到達屏障!");
concurrentHashMap.put(Thread.currentThread().getName(),1);
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName()+"繼續執行!");
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
executor.shutdown();
}
public static void main(String[] args) {
BankAccountService bankAccountService = new BankAccountService();
bankAccountService.count();
}
}
運行結果如下:
// 將線程數改爲2,沒有按預期應到4個,先到2個線程一直阻塞等待其他線程。
private final static int THREAD_COUNT = 2;
結果如圖所示:
參考資料:
Java併發編程的藝術-方騰飛
道阻且長,且歌且行! 每天一小步,踏踏實實走好腳下的路,文章爲自己學習總結,不復制黏貼,就是想讓自己的知識沉澱一下,也希望與更多的人交流,如有錯誤,請批評指正!
分
我的微信公衆號:夕陽下飛奔的豬