話題:Worker Thread模式:如何避免重複創建線程?
在上一篇文章中,我們介紹了一種最簡單的分工模式——Thread-Per-Message模式,對應到現實世界,其實就是委託代辦。這種分工模式如果用Java Thread實現,頻繁地創建、銷燬線程非常影響性能,同時無限制地創建線程還可能導致OOM,所以在Java領域使用場景就受限了。
要想有效避免線程的頻繁創建、銷燬以及OOM問題,就不得不提今天我們要細聊的,也是Java領域使用最多的Worker Thread模式。
Worker Thread模式及其實現
Worker Thread模式可以類比現實世界裏車間的工作模式:車間裏的工人,有活兒了,大家一起幹,沒活兒了就聊聊天等着。你可以參考下面的示意圖來理解,Worker Thread模式中Worker Thread對應到現實世界裏,其實指的就是車間裏的工人。不過這裏需要注意的是,車間裏的工人數量往往是確定的。
那在編程領域該如何模擬車間的這種工作模式呢?或者說如何去實現Worker Thread模式呢?通過上面的圖,你很容易就能想到用阻塞隊列做任務池,然後創建固定數量的線程消費阻塞隊列中的任務。其實你仔細想會發現,這個方案就是Java語言提供的線程池。
線程池有很多優點,例如能夠避免重複創建、銷燬線程,同時能夠限制創建線程的上限等等。學習完上一篇文章後你已經知道,用Java的Thread實現Thread-Per-Message模式難以應對高併發場景,原因就在於頻繁創建、銷燬Java線程的成本有點高,而且無限制地創建線程還可能導致應用OOM。線程池,則恰好能解決這些問題。
那我們還是以echo程序爲例,看看如何用線程池來實現。
下面的示例代碼是用線程池實現的echo服務端,相比於Thread-Per-Message模式的實現,改動非常少,僅僅是創建了一個最多線程數爲500的線程池es,然後通過es.execute()方法將請求處理的任務提交給線程池處理。
ExecutorService es = Executors.newFixedThreadPool(500);
final ServerSocketChannel ssc =
ServerSocketChannel.open().bind(new InetSocketAddress(8080));
//處理請求
try {
while (true) {
// 接收請求
SocketChannel sc = ssc.accept();
// 將請求處理任務提交給線程池
es.execute(()->{
try {
// 讀Socket
ByteBuffer rb = ByteBuffer
.allocateDirect(1024);
sc.read(rb);
//模擬處理請求
Thread.sleep(2000);
// 寫Socket
ByteBuffer wb =
(ByteBuffer)rb.flip();
sc.write(wb);
// 關閉Socket
sc.close();
}catch(Exception e){
throw new UncheckedIOException(e);
}
});
}
} finally {
ssc.close();
es.shutdown();
}
正確地創建線程池
Java的線程池既能夠避免無限制地創建線程導致OOM,也能避免無限制地接收任務導致OOM。只不過後者經常容易被我們忽略,例如在上面的實現中,就被我們忽略了。所以強烈建議你用創建有界的隊列來接收任務。
當請求量大於有界隊列的容量時,就需要合理地拒絕請求。如何合理地拒絕呢?這需要你結合具體的業務場景來制定,即便線程池默認的拒絕策略能夠滿足你的需求,也同樣建議你在創建線程池時,清晰地指明拒絕策略。
同時,爲了便於調試和診斷問題,我也強烈建議你在實際工作中給線程賦予一個業務相關的名字。
綜合以上這三點建議,echo程序中創建線程可以使用下面的示例代碼。
ExecutorService es = new ThreadPoolExecutor(
50, 500,
60L, TimeUnit.SECONDS,
//注意要創建有界隊列
new LinkedBlockingQueue<Runnable>(2000),
//建議根據業務需求實現ThreadFactory
r->{
return new Thread(r, "echo-"+ r.hashCode());
},
//建議根據業務需求實現RejectedExecutionHandler
new ThreadPoolExecutor.CallerRunsPolicy());
避免線程死鎖
使用線程池過程中,還要注意一種線程死鎖的場景。如果提交到相同線程池的任務不是相互獨立的,而是有依賴關係的,那麼就有可能導致線程死鎖。實際工作中,我就親歷過這種線程死鎖的場景。具體現象是應用每運行一段時間偶爾就會處於無響應的狀態,監控數據看上去一切都正常,但是實際上已經不能正常工作了。
這個出問題的應用,相關的邏輯精簡之後,如下圖所示,該應用將一個大型的計算任務分成兩個階段,第一個階段的任務會等待第二階段的子任務完成。在這個應用裏,每一個階段都使用了線程池,而且兩個階段使用的還是同一個線程池。
我們可以用下面的示例代碼來模擬該應用,如果你執行下面的這段代碼,會發現它永遠執行不到最後一行。執行過程中沒有任何異常,但是應用已經停止響應了。
//L1、L2階段共用的線程池
ExecutorService es = Executors.newFixedThreadPool(2);
//L1階段的閉鎖
CountDownLatch l1=new CountDownLatch(2);
for (int i=0; i<2; i++){
System.out.println("L1");
//執行L1階段任務
es.execute(()->{
//L2階段的閉鎖
CountDownLatch l2=new CountDownLatch(2);
//執行L2階段子任務
for (int j=0; j<2; j++){
es.execute(()->{
System.out.println("L2");
l2.countDown();
});
}
//等待L2階段任務執行完
l2.await();
l1.countDown();
});
}
//等着L1階段任務執行完
l1.await();
System.out.println("end");
當應用出現類似問題時,首選的診斷方法是查看線程棧。下圖是上面示例代碼停止響應後的線程棧,你會發現線程池中的兩個線程全部都阻塞在 l2.await(); 這行代碼上了,也就是說,線程池裏所有的線程都在等待L2階段的任務執行完,那L2階段的子任務什麼時候能夠執行完呢?永遠都沒那一天了,爲什麼呢?因爲線程池裏的線程都阻塞了,沒有空閒的線程執行L2階段的任務了。
原因找到了,那如何解決就簡單了,最簡單粗暴的辦法就是將線程池的最大線程數調大,如果能夠確定任務的數量不是非常多的話,這個辦法也是可行的,否則這個辦法就行不通了。其實**這種問題通用的解決方案是爲不同的任務創建不同的線程池。**對於上面的這個應用,L1階段的任務和L2階段的任務如果各自都有自己的線程池,就不會出現這種問題了。
最後再次強調一下:提交到相同線程池中的任務一定是相互獨立的,否則就一定要慎重。
總結
我們曾經說過,解決併發編程裏的分工問題,最好的辦法是和現實世界做對比。對比現實世界構建編程領域的模型,能夠讓模型更容易理解。上一篇我們介紹的Thread-Per-Message模式,類似於現實世界裏的委託他人辦理,而今天介紹的Worker Thread模式則類似於車間裏工人的工作模式。如果你在設計階段,發現對業務模型建模之後,模型非常類似於車間的工作模式,那基本上就能確定可以在實現階段採用Worker Thread模式來實現。
Worker Thread模式和Thread-Per-Message模式的區別有哪些呢?從現實世界的角度看,你委託代辦人做事,往往是和代辦人直接溝通的;對應到編程領域,其實現也是主線程直接創建了一個子線程,主子線程之間是可以直接通信的。而車間工人的工作方式則是完全圍繞任務展開的,一個具體的任務被哪個工人執行,預先是無法知道的;對應到編程領域,則是主線程提交任務到線程池,但主線程並不關心任務被哪個線程執行。
Worker Thread模式能避免線程頻繁創建、銷燬的問題,而且能夠限制線程的最大數量。Java語言裏可以直接使用線程池來實現Worker Thread模式,線程池是一個非常基礎和優秀的工具類,甚至有些大廠的編碼規範都不允許用new Thread()來創建線程的,必須使用線程池。
不過使用線程池還是需要格外謹慎的,除了今天重點講到的如何正確創建線程池、如何避免線程死鎖問題,還需要注意前面我們曾經提到的ThreadLocal內存泄露問題。同時對於提交到線程池的任務,還要做好異常處理,避免異常的任務從眼前溜走,從業務的角度看,有時沒有發現異常的任務後果往往都很嚴重。
Demo
1
public class Channel {//流水線上的貨物,流水線工人從流水線上拿貨物工作
private final static int MAX_REQUEST = 100;
private final Request[] requestQueue;
private int head;//隊頭
private int tail;//隊尾
private int count;//流水線當前的數量
private final WorkerThread[] workerPool;//流水線的工人
public Channel(int workers) {
this.requestQueue = new Request[MAX_REQUEST];
this.head = 0;
this.tail = 0;
this.count = 0;
this.workerPool = new WorkerThread[workers];//線程
this.init();
}
private void init() {
for (int i = 0; i < workerPool.length; i++) {
workerPool[i] = new WorkerThread("Worker-" + i, this);
}
}
/**
* push switch to start all of worker to work. 一推開關,流水線工人開始工作
*/
public void startWorker() {//給外面的人調用
Arrays.asList(workerPool).forEach(WorkerThread::start);
}
public synchronized void put(Request request) {//放到流水線上面去,
while (count >= requestQueue.length) {
try {
this.wait();
} catch (Exception e) {
}
}
this.requestQueue[tail] = request;
this.tail = (tail + 1) % requestQueue.length;//簡單算法,每次移動到前一個位置
this.count++;
this.notifyAll();
}
public synchronized Request take() {
while (count <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Request request = this.requestQueue[head];
this.head = (this.head + 1) % this.requestQueue.length;//
this.count--;
this.notifyAll();
return request;
}
}
2
public class Request {
private final String name;
private final int number;
public Request(final String name, final int number) {
this.name = name;
this.number = number;
}
public void execute() {
System.out.println(Thread.currentThread().getName() +
" executed " + this);//哪位工人放的,拿到該貨再執行這個方法
}
@Override
public String toString() {
return "Request=> No. " + number + " Name. " + name;
}
}
3
public class WorkerThread extends Thread {
private final Channel channel;
private static final Random random = new Random(System.currentTimeMillis());
public WorkerThread(String name, Channel channel) {
super(name);
this.channel = channel;
}
@Override
public void run() {
while (true) {
channel.take().execute();
try {
Thread.sleep(random.nextInt(1_000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4
public class WorkerClient {
public static void main(String[] args) {
final Channel channel = new Channel(5);
channel.startWorker();//拉咂開工
//搬運工開始搬貨物到流水線上面
new TransportThread("Alex", channel).start();
new TransportThread("Jack", channel).start();
new TransportThread("William", channel).start();
}
}
5
public class TransportThread extends Thread {
private final Channel channel;//往流水線放
private static final Random random = new Random(System.currentTimeMillis());
//裝配工人,搬運工,放到流水線上面
public TransportThread(String name, Channel channel) {
super(name);
this.channel = channel;
}
@Override
public void run() {
try {
for (int i = 0; true; i++) {
Request request = new Request(getName(), i);
this.channel.put(request);
Thread.sleep(random.nextInt(1_000));
}
} catch (Exception e) {
}
}
}