0.背景
現在有一個大資料平臺,我們需要透過spark對hive裡的資料讀取清洗轉換(etl)再加其它的業務操作的過程,然後需要把這批資料落地到tbase資料庫(騰訊的一款分散式資料庫)。
資料匯入的特點是不定時,但量大。每次匯入的資料量在幾億到幾十億上百億之間。
如果使用dataset.write的方式寫入,spark內部也是使用的sql connection以jdbc的方式進行寫入。在這樣的資料量之下,會非常慢,慢到完全無法接受。
經研究,tbase底層為pgsql,支援以檔案的方式copy寫入。
語法為:
COPY table FROM '/mnt/g/file.csv' WITH CSV HEADER;
這樣效率高了很多。
經過測試,十億級別的資料在半小時單位就能夠寫入。當然,建立了索引,以及隨著表資料量的增大,寫入效率會降低,但完全能夠接受。
那麼,現在就是使用spark讀取hive,經過處理,再dataset.repartion(num)重分割槽,將資料寫入HDFS形成num個檔案。再將這些小檔案多執行緒批次copy到tbds。
hdfs小檔案數量nums從幾千到幾萬,而批次寫入的連線數connections不可能無限大,
把檔案抽象成生產者,資料庫連線抽象成消費者。生產者源源不斷生產,消費者能力有限跟不上生產者的速率,就需要阻塞在消費端。
1.實現方式
生產者-消費者模式的實現,不論是自己使用鎖,還是使用阻塞佇列,其核心都是阻塞。
1.1 方式1 執行緒池自帶阻塞佇列
我們批次寫入是透過多執行緒來的,實現一個執行緒池的其中之一方法是透過Executors
,並指定一個帶執行緒數的引數。
這樣的方式線上上7*24小時執行的業務系統中是絕對不推薦使用的,但在一些大資料平臺的定時任務也不是完全禁止,看自身情況。
使用Executors構建執行緒池最大問題在於它底層也是透過ThreadPoolExecutor
來構建執行緒池,核心執行緒和最大執行緒相同,且阻塞佇列預設為LinkedBlockingQueue
,這個阻塞佇列
沒有設定長度,那麼它的最大長度為Integer.MAX_VALUE
。
這樣就可能造成記憶體的無限增長,記憶體耗盡導致OOM。
但具體到我們現在的這個場景下,檔案數為幾千到幾萬,那麼執行緒池阻塞佇列的長度在這個範圍以內,如果平臺資源能夠接受,也不是不可以。
同時,剛好可以利用執行緒池的阻塞佇列來構建消費者-生產者。
public static void main(String[] args) throws Exception {
List<File> fileList = cn.hutool.core.io.FileUtil.loopFiles(new File("測試路徑"));
ExecutorService executorService = Executors.newFixedThreadPool(10);
LongAdder longAdder = new LongAdder();
for(File file : fileList){
try {
executorService.execute(new TestRun(fileList, longAdder));
} catch (Exception exception) {
exception.printStackTrace();
}
}
executorService.shutdown();
}
public static class TestRun implements Runnable{
private List<File> fileList;
LongAdder longAdder;
public TestRun(List<File> fileList, LongAdder longAdder) {
this.fileList = fileList;
this.longAdder = longAdder;
}
@SneakyThrows
@Override
public void run() {
try {
// 可透過連線池
longAdder.increment();
ConnectionUtils.getConnection();
System.out.println(Thread.currentThread() + "第"+ longAdder.longValue() + "/"+ fileList.size() +"個檔案獲取連線正在入庫");
Random random = new Random();
Thread.sleep(random.nextInt(1000));
System.out.println(Thread.currentThread() + "第"+ longAdder.longValue() + "/"+ fileList.size() +"個檔案完成入庫歸還連線");
} finally {
}
}
}
執行輸出:
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
Thread[pool-1-thread-5,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-9,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-1,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-2,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-7,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-10,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-6,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-8,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-4,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-3,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-1,5,main]第10/33個檔案完成入庫歸還連線
資料庫驅動載入成功
Thread[pool-1-thread-1,5,main]第11/33個檔案獲取連線正在入庫
Thread[pool-1-thread-4,5,main]第11/33個檔案完成入庫歸還連線
資料庫驅動載入成功
.
.
.
資料庫驅動載入成功
Thread[pool-1-thread-3,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-9,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-8,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-6,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-7,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-10,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-5,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-4,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-3,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-2,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-1,5,main]第33/33個檔案完成入庫歸還連線
這裡的longAdder只是為了方便觀看,並沒有嚴格按執行緒遞增。
我們模擬33個檔案,執行緒池的核心大小為10,可以看到最大隻有10個檔案在同時執行,只有當其中檔案入庫完畢,新的檔案才能執行。達到了我們想要的效果。
1.2 方式2 使用阻塞佇列+CountDownLatch
CountDownLatch是什麼?
它是一種同步輔助工具,允許一個或多個執行緒等待,直到在其他執行緒中執行的一組操作完成。
CountDownLatch使用給定的計數進行初始化。await()會阻塞,直到當前計數由於countDown()的呼叫而達到零,之後所有等待執行緒都會被釋放,任何後續的await()呼叫都會立即返回。這是一種一次性現象——計數無法重置。
CountDownLatch是一種通用的同步工具,可用於多種目的。用計數1初始化的CountDownLatch用作簡單的開/關鎖存器或門:所有呼叫的執行緒都在門處等待,直到呼叫countDown的執行緒開啟它。初始化為N的CountDownLatch可以用來讓一個執行緒等待,直到N個執行緒完成了一些操作,或者一些操作已經完成了N次。
自定義一個阻塞佇列,並將這個阻塞佇列構建成資料庫連線池,使用10個固定的大小,只有檔案take到連線才會入庫操作,拿不到的時候就阻塞直到其它檔案入庫完成歸還資料庫連線。
@Slf4j
public class ConnectionQueue {
LinkedBlockingQueue<Connection> connections = null;
private int size = 10;
public ConnectionQueue(int size) throws Exception{
new ConnectionQueue(null, size);
}
public ConnectionQueue(LinkedBlockingQueue<Connection> connections, int size) throws IllegalArgumentException{
if (size <= 0 || size > 100) {
throw new IllegalArgumentException("size 長度必須適宜,在1-100之間");
}
this.connections = connections;
this.size = size;
}
/**
* 初始化資料庫連線
*/
public void init(){
if (connections == null) {
connections = new LinkedBlockingQueue<>(size);
}
for (int i = 0; i < size; i++) {
connections.add(ConnectionUtils.getConnection());
}
}
/**
* 獲取一個資料庫連線,如果沒有空閒連線將阻塞直到拿到連線
* @return
* @throws InterruptedException
*/
public Connection get() throws InterruptedException {
return connections.take();
}
public Connection poll() throws InterruptedException {
return connections.poll();
}
/**
* 歸還空閒連線
* @param connection
*/
public void put(Connection connection){
connections.add(connection);
}
public int size(){
return connections.size();
}
/**
* 銷燬
*/
public void destroy() {
Iterator<Connection> it = connections.iterator();
while (it.hasNext()) {
Connection conn = it.next();
if (conn != null) {
try {
conn.close();
log.info("關閉連線 " + conn);
} catch (SQLException e) {
log.error("關閉連線失敗", e);
}
} else {
log.info("conn = {}為空", conn);
}
}
if (connections != null) {
connections.clear();
}
}
}
同時使用CountDownLatch進行計數,await()直到所有執行緒都執行完畢,再進行資源銷燬和其它業務操作。
public static void main(String[] args) throws Exception {
List<File> fileList = cn.hutool.core.io.FileUtil.loopFiles(new File("測試路徑"));
ConnectionQueue connectionQueue = new ConnectionQueue(10);
connectionQueue.init();
ExecutorService executorService = new ThreadPoolExecutor(10,
10,
1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(10),
(r, executor) -> {
if (r instanceof Test.TestRun) {
((TestRun) r).getCountDownLatch().countDown();
}
System.out.println(Thread.currentThread() +" reject countdown");
}
);
CountDownLatch countDownLatch = new CountDownLatch(fileList.size());
for(File file : fileList){
try {
Connection conn = connectionQueue.get();
executorService.execute(new TestRun(countDownLatch, connectionQueue, fileList, conn));
} catch (Exception exception) {
exception.printStackTrace();
}
}
countDownLatch.await();
executorService.shutdown();
connectionQueue.destroy();
}
public static class TestRun implements Runnable{
private CountDownLatch countDownLatch;
private ConnectionQueue connectionQueue;
private Connection connection;
private List<File> fileList;
public TestRun(CountDownLatch countDownLatch, ConnectionQueue connectionQueue, List<File> fileList, Connection connection) {
this.countDownLatch = countDownLatch;
this.connectionQueue = connectionQueue;
this.fileList = fileList;
this.connection = connection;
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@SneakyThrows
@Override
public void run() {
try {
System.out.println(Thread.currentThread() + "第"+ countDownLatch.getCount() + "/"+ fileList.size() +"個檔案獲取連線正在入庫");
Random random = new Random();
Thread.sleep(random.nextInt(1000));
System.out.println(Thread.currentThread() + "第"+ countDownLatch.getCount() + "/"+ fileList.size() +"個檔案完成入庫歸還連線");
} finally {
connectionQueue.put(connection);
countDownLatch.countDown();
}
}
}
執行結果:
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
資料庫驅動載入成功
Thread[pool-1-thread-1,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-4,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-3,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-2,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-10,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-6,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-7,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-8,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-9,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-5,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-4,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-4,5,main]第32/33個檔案獲取連線正在入庫
Thread[pool-1-thread-8,5,main]第32/33個檔案完成入庫歸還連線
Thread[pool-1-thread-8,5,main]第31/33個檔案獲取連線正在入庫
Thread[pool-1-thread-8,5,main]第31/33個檔案完成入庫歸還連線
Thread[pool-1-thread-8,5,main]第30/33個檔案獲取連線正在入庫
Thread[pool-1-thread-4,5,main]第30/33個檔案完成入庫歸還連線
...
Thread[pool-1-thread-2,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-5,5,main]第10/33個檔案完成入庫歸還連線
Thread[pool-1-thread-4,5,main]第9/33個檔案完成入庫歸還連線
Thread[pool-1-thread-9,5,main]第8/33個檔案完成入庫歸還連線
Thread[pool-1-thread-2,5,main]第7/33個檔案完成入庫歸還連線
Thread[pool-1-thread-6,5,main]第6/33個檔案完成入庫歸還連線
Thread[pool-1-thread-7,5,main]第5/33個檔案完成入庫歸還連線
Thread[pool-1-thread-10,5,main]第4/33個檔案完成入庫歸還連線
Thread[pool-1-thread-3,5,main]第3/33個檔案完成入庫歸還連線
Thread[pool-1-thread-1,5,main]第2/33個檔案完成入庫歸還連線
Thread[pool-1-thread-8,5,main]第1/33個檔案完成入庫歸還連線
1.2.1 如果執行緒池觸發reject會發生什麼?
需要注意的是,這裡要考慮到執行緒池的拒絕策略。
我們知道JDK執行緒池拒絕策略實現了四種:
AbortPolicy 預設策略,丟擲異常
CallerRunsPolicy 從名字上可以看出,呼叫者執行
DiscardOldestPolicy 丟棄最老的任務,再嘗試執行
DiscardPolicy 直接丟棄不做任何操作
ThreadPoolExecutor預設拒絕策略為AbortPolicy,就是丟擲一個異常,那麼這時候就執行不到後面的countdown
。
所以需要重寫策略,線上程池佇列已滿拒絕新進任務的時候執行countdown
,避免countDownLatch.await()
永遠等待。
如果使用預設的拒絕策略,執行如下:
1.3 方式3 使用Semaphore
在 java 中,使用了 synchronized
關鍵字和 Lock
鎖實現了資源的併發訪問控制,在同一時刻只允許一個執行緒進入臨界區訪問資源 (讀鎖除外)。但考慮到另外一種場景,共享資源在同一時刻可以提供給多個執行緒訪問,如廁所有多個坑位,可以同時提供給多人使用。這種場景下,就可以使用Semaphore
訊號量來實現。
訊號量通常用於限制可以訪問某些(物理或邏輯)資源的執行緒數量。訊號量維護一組許可(permit),在訪問資源前,每個執行緒必須從訊號量獲得一個許可,以保證資源的有限訪問。當執行緒處理完後,向訊號量返回一個許可,允許另一個執行緒獲取。
當訊號量許可>1,意味可以訪問資源,如果訊號量許可<=0,執行緒進入休眠。
當訊號量許可=1,約等於synchronized
或lock
的效果。
就好比一個廁所管理員,站在門口,只有廁所有空位,就開門允許與空側數量等量的人進入廁所。多個人進入廁所後,相當於N個人來分配使用N個空位。為避免多個人來同時競爭同一個側衛,在內部仍然使用鎖來控制資源的同步訪問。
在我們的場景下,共享資源就是資料庫連線池N個,M個檔案需要拿到連線池進行入庫操作,但連線池數量N有限,遠小於檔案數M,所以需要對連線池的訪問併發度進行控制。
訊號量在這裡起到了控流的作用。
Semaphore semaphore = new Semaphore(10);
允許執行緒池最多10個任務並行執行,只有當其它任務執行完畢歸還permit,新的任務拿到permit才能開始執行。
public static void main(String[] args) throws Exception {
List<File> fileList = FileUtil.loopFiles(new File("測試路徑"));
Semaphore semaphore = new Semaphore(10);
Random random = new Random();
ExecutorService executorService = new ThreadPoolExecutor(10,
10,
1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(10));
AtomicInteger count = new AtomicInteger(1);
for (File file : fileList) {
semaphore.acquire();
executorService.execute(() -> {
try {
int subCount = count.getAndIncrement();
System.out.println(Thread.currentThread() + "第" + subCount + "/" + fileList.size() + "個檔案獲取連線正在入庫");
// 模擬入庫操作
int time = random.nextInt(1000);
Thread.sleep(time);
System.out.println(Thread.currentThread() + "第" + subCount + "/" + fileList.size() + "個檔案完成入庫歸還連線");
} catch (Exception e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
System.out.println("shutdown");
executorService.shutdown();
}
因為我們的大資料框架本身有獲取連線池的輪子,這裡省略了從連線池獲取連線的操作。
執行日誌:
Thread[pool-1-thread-1,5,main]第1/33個檔案獲取連線正在入庫
Thread[pool-1-thread-3,5,main]第3/33個檔案獲取連線正在入庫
Thread[pool-1-thread-4,5,main]第2/33個檔案獲取連線正在入庫
Thread[pool-1-thread-10,5,main]第5/33個檔案獲取連線正在入庫
Thread[pool-1-thread-9,5,main]第4/33個檔案獲取連線正在入庫
Thread[pool-1-thread-8,5,main]第8/33個檔案獲取連線正在入庫
Thread[pool-1-thread-2,5,main]第9/33個檔案獲取連線正在入庫
Thread[pool-1-thread-7,5,main]第7/33個檔案獲取連線正在入庫
Thread[pool-1-thread-6,5,main]第6/33個檔案獲取連線正在入庫
Thread[pool-1-thread-5,5,main]第10/33個檔案獲取連線正在入庫
Thread[pool-1-thread-5,5,main]第10/33個檔案完成入庫歸還連線
Thread[pool-1-thread-5,5,main]第11/33個檔案獲取連線正在入庫
Thread[pool-1-thread-3,5,main]第3/33個檔案完成入庫歸還連線
...
Thread[pool-1-thread-2,5,main]第23/33個檔案完成入庫歸還連線
shutdown
Thread[pool-1-thread-2,5,main]第33/33個檔案獲取連線正在入庫
Thread[pool-1-thread-4,5,main]第24/33個檔案完成入庫歸還連線
Thread[pool-1-thread-5,5,main]第32/33個檔案完成入庫歸還連線
Thread[pool-1-thread-1,5,main]第30/33個檔案完成入庫歸還連線
Thread[pool-1-thread-9,5,main]第26/33個檔案完成入庫歸還連線
Thread[pool-1-thread-3,5,main]第19/33個檔案完成入庫歸還連線
Thread[pool-1-thread-2,5,main]第33/33個檔案完成入庫歸還連線
Thread[pool-1-thread-8,5,main]第22/33個檔案完成入庫歸還連線
Thread[pool-1-thread-6,5,main]第27/33個檔案完成入庫歸還連線
Thread[pool-1-thread-10,5,main]第31/33個檔案完成入庫歸還連線
Thread[pool-1-thread-7,5,main]第28/33個檔案完成入庫歸還連線
1.3.1 如果引發了預設執行緒池拒絕策略,Semaphore會有問題嗎?
我們知道CountDownLatch由於執行緒池拒絕策略,沒有執行到countdown()會導致程式一直阻塞。那麼Semaphore會有相應的問題嗎?
如果執行緒池佇列滿了,觸發了預設拒絕策略,這時候,Semaphore執行了acquire()
,但沒執行release()
。
寫一個測試例子:
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(20);
Semaphore semaphore = new Semaphore(10);
ExecutorService executorService = new ThreadPoolExecutor(5,
5,
1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1), (r, executor) -> {
Random random = new Random();
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (r instanceof TestRun) {
((TestRun) r).getCountDownLatch().countDown();
// ((TestRun) r).getSemaphore().release();
}
System.out.println(Thread.currentThread() + " reject countdown " + semaphore.availablePermits());
});
for (int i = 0; i < 30; i++) {
semaphore.acquire();
Thread.sleep(100);
executorService.execute(new TestRun(countDownLatch, semaphore));
}
// countDownLatch.await();
System.out.println("完成");
executorService.shutdown();
}
public static class TestRun implements Runnable {
private CountDownLatch countDownLatch;
private Semaphore semaphore;
public TestRun(CountDownLatch countDownLatch, Semaphore semaphore) {
this.countDownLatch = countDownLatch;
this.semaphore = semaphore;
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public Semaphore getSemaphore() {
return semaphore;
}
public void setSemaphore(Semaphore semaphore) {
this.semaphore = semaphore;
}
@SneakyThrows
@Override
public void run() {
// semaphore.acquire();
Random random = new Random();
Thread.sleep(random.nextInt(1000));
countDownLatch.countDown();
semaphore.release();
System.out.println(Thread.currentThread() + " start" + " semaphore = " + semaphore.availablePermits());
System.out.println(Thread.currentThread() + " countdown");
}
}
執行日誌:
Thread[pool-1-thread-1,5,main] start semaphore = 8
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 5
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 4
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 5
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 6
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 7
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 7
Thread[pool-1-thread-4,5,main] start semaphore = 5
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 5
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 4
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 3
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 3
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 4
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 4
Thread[pool-1-thread-4,5,main] start semaphore = 4
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 4
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 3
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 3
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 2
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 2
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 2
Thread[pool-1-thread-3,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 3
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 4
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-5,5,main] start semaphore = 5
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 6
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 6
完成
Thread[pool-1-thread-5,5,main] start semaphore = 4
Thread[pool-1-thread-5,5,main] countdown
Thread[pool-1-thread-2,5,main] start semaphore = 5
Thread[pool-1-thread-2,5,main] countdown
Thread[pool-1-thread-4,5,main] start semaphore = 6
Thread[pool-1-thread-4,5,main] countdown
Thread[pool-1-thread-3,5,main] start semaphore = 7
Thread[pool-1-thread-3,5,main] countdown
可以看到執行了3次reject,最後semaphore值為7,正常應該為初始值10。
首先程式能夠正常執行完畢,然後併發度下降了。
如果極端情況下,觸發拒絕策略增多,semaphore的值降為1,這裡semaphore
就變成了lock
或者synchronized
,多執行緒就失去了效果變成了單執行緒序列執行。
透過JDK執行緒池拒絕策略之一的CallerRunsPolicy
原始碼可知,這裡的r
即為呼叫者執行緒,在這裡就是main執行緒。我們在main執行緒執行了acquire()
,那麼我們只需要重寫拒絕策略,在這裡執行release()
就可保證併發度與初始值保持一致。
但是如果semaphore=0呢?會阻塞執行嗎?
1.3.2 如果初始化的時候就為0
Semaphore semaphore = new Semaphore(0);
那麼程式會永遠阻塞不執行,因為沒有可用的permit。
jdk原始碼這裡沒有對傳入的引數做判斷,甚至可以傳入負數。
因為與countdownlatch不同,這裡可以釋放增加任意大於0的permit數量。
1.3.3 如果reject次數大於等於初始化長度
初化長度大於1,比如10,
Semaphore semaphore = new Semaphore(10);
同時,執行緒池拒絕次數>= 10,理論上,這個時候Semaphore就會出現0或負數。
執行緒就會阻塞。
但這種情況真的會發生嗎?
我模擬了很多次都沒出現阻塞的情況。
把執行緒池大小調整為1,將Semaphore大小設定為>1,這裡為4。
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(20);
Semaphore semaphore = new Semaphore(4);
ExecutorService executorService = new ThreadPoolExecutor(1,
1,
1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(1), (r, executor) -> {
Random random = new Random();
try {
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (r instanceof TestRun) {
((TestRun) r).getCountDownLatch().countDown();
// ((TestRun) r).getSemaphore().acquire();
// ((TestRun) r).getSemaphore().release();
}
System.out.println(Thread.currentThread() + " reject countdown " + semaphore.availablePermits());
});
for (int i = 0; i < 30; i++) {
semaphore.acquire();
// Thread.sleep(100);
executorService.execute(new TestRun(countDownLatch, semaphore));
}
// countDownLatch.await();
System.out.println("完成");
executorService.shutdown();
}
public static class TestRun implements Runnable {
private CountDownLatch countDownLatch;
private Semaphore semaphore;
public TestRun(CountDownLatch countDownLatch, Semaphore semaphore) {
this.countDownLatch = countDownLatch;
this.semaphore = semaphore;
}
public CountDownLatch getCountDownLatch() {
return countDownLatch;
}
public void setCountDownLatch(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public Semaphore getSemaphore() {
return semaphore;
}
public void setSemaphore(Semaphore semaphore) {
this.semaphore = semaphore;
}
@SneakyThrows
@Override
public void run() {
// semaphore.acquire();
Random random = new Random();
Thread.sleep(random.nextInt(1000));
countDownLatch.countDown();
semaphore.release();
System.out.println(Thread.currentThread() + " start" + " semaphore = " + semaphore.availablePermits());
System.out.println(Thread.currentThread() + " countdown");
}
}
執行結果:
Thread[pool-1-thread-1,5,main] start semaphore = 2
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 2
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 1
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[main,5,main] reject countdown 0
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 0
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
完成
Thread[pool-1-thread-1,5,main] start semaphore = 1
Thread[pool-1-thread-1,5,main] countdown
最後semaphore = 1.
當我將semaphore初始化值調整為3,5,2,最後semaphore的值總是為1。
執行緒池觸發拒絕次數總是為semaphore初始化值-1
。
其實也很好理解,因為當permit>=1的時候,acquire()方法才會返回,不然就一直阻塞。所以初始permit>0的情況下,永遠不會出現permit為0。
所以,結論是隻要semaphore的初始值大於0,就不用擔心程式會一直阻塞不執行。
同時,執行緒池觸發拒絕策略,如果沒有重寫拒絕策略執行semaphore.release()
,就會將併發度降低。
2. 總結
1.直接使用執行緒池佇列要注意阻塞佇列大小為Integer.MAX_VALUE可能導致記憶體消耗問題。
2.這裡使用訊號量最為簡單便捷。
3.不管使用的是coundownlatch還是訊號量,都要注意執行緒池拒絕的情況。
如果countdownlatch因為執行緒池拒絕策略沒有執行countdown會導致await一直等待阻塞;
如果訊號量因為執行緒池拒絕策略沒有執行release,導致沒有足夠的permit,不會導致程式阻塞,但會降低併發 度。