使用執行緒執行框架的一次經歷
場景
一個執行緒從某個地方接收訊息(資料),可以是其他主機或者訊息佇列,然後轉由另外的一個執行緒池來執行具體處理訊息的邏輯,並且訊息的處理速度小於接收訊息的速度。這種情景很常見,試想一下,你會怎麼設計和實現?
直觀想法
很顯然採用JUC的執行緒框架,可以迅速寫出程式碼。
訊息接收者:
public class Receiver { private static volatile boolean inited = false; private static volatile boolean shutdown = false; private static volatile int cnt = 0; private MessageHandler messageHandler; public void start(){ Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { while(!shutdown){ init(); recv(); } } }); } /** * 模擬訊息接收 */ public void recv(){ Message msg = new Message("Msg" + System.currentTimeMillis()); System.out.println(String.format("接收到訊息(%d): %s", ++cnt, msg)); messageHandler.handle(msg); } public void init(){ if(!inited){ messageHandler = new MessageHandler(); inited = true; } } public static void main(String[] args) { new Receiver().start(); } }
訊息處理:
public class MessageHandler { private static final int THREAD_POOL_SIZE = 4; private ExecutorService service = Executors.newFixedThreadPool(THREAD_POOL_SIZE); public void handle(Message msg) { try { service.execute(new Runnable() { @Override public void run() { parseMsg(msg); } }); } catch (Throwable e) { System.out.println("訊息處理異常" + e); } } /** * 比較耗時的訊息處理流程 */ public void parseMsg(Message message) { while (true) { try { System.out.println("解析訊息:" + message); Thread.sleep(5000); System.out.println("============================"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
效果:這種方案導致的現象是接收到的訊息會迅速堆積,我們從訊息佇列(或者其他地方)取出了大量訊息,但是處理執行緒的速度又跟不上,所以導致的問題是大量的Task會堆積線上程池底層維護的一個阻塞佇列中,這會極大的耗費儲存空間,影響系統的效能。
分析:當execute()一個任務的時候,如果有空閒的worker執行緒,那麼投入執行,否則看設定的最大執行緒個數,沒有達到執行緒個數限制就建立新執行緒,接新任務,否則就把任務緩衝到一個阻塞佇列中,問題就是這個佇列,預設的大小是沒有限制的,所以就會大量的堆積任務,必然耗費heap空間。
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } public LinkedBlockingQueue() { this(Integer.MAX_VALUE); // capacity }
計數限制
面對上述問題,想到了要限制訊息接收的速度,自然就想到了各種執行緒同步的原語,不過在這裡最簡單的就是使用一個Volatile的計數器。
訊息接收者:
public class Receiver { private static volatile boolean inited = false; private static volatile boolean shutdown = false; private static volatile int cnt = 0; private MessageHandler messageHandler; public void start(){ Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { while(!shutdown){ init(); recv(); } } }); } /** * 模擬訊息接收 */ public void recv(){ Message msg = new Message("Msg" + System.currentTimeMillis()); System.out.println(String.format("接收到訊息(%d): %s", ++cnt, msg)); messageHandler.handle(msg); } public void init(){ if(!inited){ messageHandler = new MessageHandler(); inited = true; } } public static void main(String[] args) { new Receiver().start(); } }
訊息處理:
public class MessageHandler { private static final int THREAD_POOL_SIZE = 1; private ExecutorService service = Executors.newFixedThreadPool(THREAD_POOL_SIZE); public void handle(Message msg){ try { service.execute(new Runnable() { @Override public void run() { parseMsg(msg); } }); } catch (Throwable e) { System.out.println("訊息處理異常" + e); } } /** * 比較耗時的訊息處理流程 */ public void parseMsg(Message message){ try { Thread.sleep(10000); System.out.println("解析訊息:" + message); } catch (InterruptedException e) { e.printStackTrace(); }finally { Receiver.limit --; } } }
效果:通過控制訊息的個數來阻塞訊息的接收過程,就不會導致任務的堆積,系統的記憶體消耗會比較平緩,限制訊息的個數本質就和下面限制任務佇列大小一樣。
使用同步佇列 SynchronousQueue
SynchronousQueue 雖名為佇列,但是其實不會緩衝任務的物件,只是作為物件傳遞的控制點,如果有空閒執行緒或者沒有達到最大執行緒限制,就會交付給worker執行緒去執行,否則就會拒絕,我們需要自己實現對應的拒絕策略RejectedExecutionHandler,預設的是丟擲異常RejectedExecutionException。
訊息接收者同上。
訊息處理:
public class MessageHandler { private static final int THREAD_POOL_SIZE = 4; ThreadPoolExecutor service = new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println("自定義拒絕策略"); try { executor.getQueue().put(r); System.out.println("重新放任務回佇列"); } catch (InterruptedException e) { e.printStackTrace(); } } }); public void handle(Message msg) { try { System.out.println(service.getTaskCount()); System.out.println(service.getQueue().size()); System.out.println(service.getCompletedTaskCount()); service.execute(new Runnable() { @Override public void run() { parseMsg(msg); } }); } catch (Throwable e) { System.out.println("訊息處理異常" + e); } } /** * 比較耗時的訊息處理流程 */ public void parseMsg(Message message) { while (true) { try { System.out.println("執行緒名:" + Thread.currentThread().getName()); System.out.println("解析訊息:" + message); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
效果:能夠控制訊息的接收速度,但是我們需要在rejectedExecution中實現某種阻塞的操作,但是選擇在發生拒絕的時候把任務重新放回佇列,帶來的問題就是這個Task會發生飢餓現象。
使用大小限制的阻塞佇列
使用LinkedBlockingQueue作為執行緒框架底層的任務緩衝區,並且設定大小限制,思想上和上述方案一樣,都是有一個阻塞的點,但是通過最後的jvm monitor看到這裡的CPU消耗更少,記憶體使用有所降低,並且波動小(具體原因有待探索)。
訊息接收者同上。
訊息處理:
public class MessageHandler { private static final int THREAD_POOL_SIZE = 4; private static final int BLOCK_QUEUE_CAP = 500; ThreadPoolExecutor service = new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(BLOCK_QUEUE_CAP), new SimpleThreadFactory(), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println("自定義拒絕策略"); try { executor.getQueue().put(r); System.out.println("重新放任務回佇列"); } catch (InterruptedException e) { e.printStackTrace(); } } }); public void handle(Message msg) { try { service.execute(new Runnable() { @Override public void run() { parseMsg(msg); } }); } catch (Throwable e) { System.out.println("訊息處理異常" + e); } } /** * 比較耗時的訊息處理流程 */ public void parseMsg(Message message) { try { Thread.sleep(5000); System.out.println("執行緒名:" + Thread.currentThread().getName()); System.out.println("解析訊息:" + message); } catch (InterruptedException e) { e.printStackTrace(); } } static class SimpleThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("Thread-" + System.currentTimeMillis()); return thread; } } }
總結
多執行緒是比較容易出問題的地方,特別當對方法不熟悉的時候
相關文章
- Java多執行緒-執行緒池的使用Java執行緒
- Android程式框架:執行緒與執行緒池Android框架執行緒
- java多執行緒之執行緒的基本使用Java執行緒
- 多執行緒-執行緒組的概述和使用執行緒
- 多執行緒-執行緒池的概述和使用執行緒
- java執行緒之守護執行緒和使用者執行緒Java執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- MFC多執行緒的建立,包括工作執行緒和使用者介面執行緒執行緒
- 多執行緒-執行緒控制之休眠執行緒執行緒
- 多執行緒-執行緒控制之加入執行緒執行緒
- 多執行緒-執行緒控制之禮讓執行緒執行緒
- 多執行緒-執行緒控制之中斷執行緒執行緒
- 執行緒池的使用執行緒
- 執行緒的基本使用執行緒
- 執行緒、開啟執行緒的兩種方式、執行緒下的Join方法、守護執行緒執行緒
- 保證執行緒在主執行緒執行執行緒
- 多執行緒-執行緒控制之守護執行緒執行緒
- Java多執行緒學習(八)執行緒池與Executor 框架Java執行緒框架
- 執行緒的建立及執行緒池執行緒
- 執行緒和執行緒池執行緒
- 多執行緒【執行緒池】執行緒
- 多執行緒--執行緒管理執行緒
- Java多執行緒——執行緒Java執行緒
- 執行緒 執行緒池 Task執行緒
- 執行緒與多執行緒執行緒
- Java 執行緒 Executor 框架詳解與使用Java執行緒框架
- 多執行緒使用執行緒
- 一個執行緒,從“生”到“死”經歷的過程執行緒
- 執行緒池和Executor框架執行緒框架
- .net使用Task多執行緒執行任務 .net限制執行緒數量執行緒
- iOS多執行緒全套:執行緒生命週期,多執行緒的四種解決方案,執行緒安全問題,GCD的使用,NSOperation的使用iOS執行緒GC
- 多執行緒:執行緒池理解和使用總結執行緒
- 多執行緒程式設計基礎(一)-- 執行緒的使用執行緒程式設計
- Java執行緒篇——執行緒的開啟Java執行緒
- 多執行緒(五)---執行緒的Yield方法執行緒
- 執行緒池建立執行緒的過程執行緒
- 【Java多執行緒】執行緒安全的集合Java執行緒
- Android的執行緒和執行緒池Android執行緒