淺談JAVA ThreadPoolExecutor

原文地址:http://blog.csdn.net/historyasamirror/article/details/5961368

這篇文章分爲兩部分,前面是ThreadPoolExecutor的一些基本知識,後一部分則是Mina中一個特殊的ThreadPoolExecutor代碼解析。算是我的Java學習筆記吧。

 

基礎

在我看來,java比C++的一個大好處就是提供了對多線程的支持(C++只有多線程的庫,語言本身不包含線程的概念)。而其中我最愛用的就是ThreadPoolExecutor這個類,它實現了一個非常棒的thread pool。
thread pool一般被用來解決兩個問題:當處理大量的同步task的時候,它能夠避免thread不斷創建銷燬的開銷;而另外一個也許更重要的含義是,它其實表示了一個boundary,通過使用thread pool可以限制這些任務所消耗的資源,比如最大線程數,比如最大的消息緩衝池。
需要指出的是,ThreadPoolExecutor不僅僅是簡單的多個thread的集合,它還帶有一個消息隊列。

在Java中,如果只是需要一個簡單的thread pool,ExecuteService可能更爲合適,這是一個Interface。可以通過調用Executor的靜態方法來獲得一些簡單的threadpool,如:

[java] view plaincopy
  1. ExecuteService pool = Executors.newFixedThreadPool(poolSize);  

但如果要用定製的thread pool,則要使用ThreadPoolExecutor類,這是一個高度可定製的線程池類,下面是一些重要的參數和方法:

 

corePoolSize 和 maxPoolSize

這兩個參數其實和threadpool的調度策略密切相關:
如果poolsize小於coresize,那麼只要來了一個request,就新創建一個thread來執行;
如果poolsize已經大於或等於coresize,那麼來了一個request後,就放進queue中,等來線程執行;
一旦且只有queue滿了,纔會又創建新的thread來執行;
當然,coresize和maxpoolsize可以在運行時通過set方法來動態的調節;
(queue如果是一個確定size的隊列,那麼很有可能發生reject request的事情(因爲隊列滿了)。很多人會認爲這樣的系統不好。但其實,reject request很多時候是個好事,因爲當負載大於系統的capacity的時候,如果不reject request,系統會出問題的。)

 

ThreadFactory 
可以通過設置默認的ThreadFactory來改變threadpool如何創建thread

keep-alive time 
如果實際的線程數大於coresize,那麼這些超額的thread過了keep-alive的時間之後,就會被kill掉。這個時間是可以動態設定的;

queue 
任何一個BlockingQueue都可以做爲threadpool中的隊列,又可以分爲三種:
AsynchronousQueue,採用這種queue,任何的task會被直接交到thread手中,queue本身不緩存任何的task,所以如果所有的線程在忙的話,新進入的task是會被拒絕的;
LinkedBlockingQueue,queue的size是無限的,根據前面的調度策略可知,thread的size永遠也不會大於coresize;
ArrayBlockingQueue,這其實是需要仔細調整參數的一種方式。因爲通過設定maxsize和queuesize,其實就是設定這個threadpool所能使用的resource,然後試圖達到一種性能的最優;(Queue sizes and maximum pool sizes may be traded off for each other: Using large queues and small pools minimizes CPU usage, OS resources, and context-switching overhead, but can lead to artificially low throughput. If tasks frequently block (for example if they are I/O bound), a system may be able to schedule time for more threads than you otherwise allow. Use of small queues generally requires larger pool sizes, which keeps CPUs busier but may encounter unacceptable scheduling overhead, which also decreases throughput. )

 

此外,還有諸如beforeExecute,afterExecute等方法可以被重寫。以上的這些內容其實都可以在ThreadPoolExecutor的javadoc中找到。應該說,ThreadPoolExecutor是可以非常靈活的被設置的,只除了一點,你沒辦法改變它的調度策略。

 

一個實例

通過分析一個特殊的ThreadPoolExeuctor的源代碼,能夠更好的理解它的內部機制和靈活性。

Mina中有一個特殊的ThreadPoolExecutor--org.apache.mina.filter.executor.OrderedThreadPoolExecutor。
這個executor是用來處理從網絡中來的請求。它的不同之處在於,對於同一個session來的請求,它能夠按照請求到達的時間順序的執行。舉個例子,在一個session中,如果先接收到request A,然後再接收到request B,那麼,OrderedThreadPoolExecutor能夠保證一定處理完A之後再處理B。而一般的thread pool,會將A和B傳遞給不同的thread處理,很有可能request B會先於request A完成。

 

先看看它的構造函數:

[java] view plaincopy
  1. public OrderedThreadPoolExecutor(  
  2.             int corePoolSize, int maximumPoolSize,  
  3.             long keepAliveTime, TimeUnit unit,  
  4.             ThreadFactory threadFactory, IoEventQueueHandler eventQueueHandler) {  
  5.         // We have to initialize the pool with default values (0 and 1) in order to  
  6.         // handle the exception in a better way. We can't add a try {} catch() {}  
  7.         // around the super() call.  
  8.         super(DEFAULT_INITIAL_THREAD_POOL_SIZE, 1, keepAliveTime, unit,   
  9.             new SynchronousQueue<Runnable>(), threadFactory, new AbortPolicy());  
  10.         if (corePoolSize < DEFAULT_INITIAL_THREAD_POOL_SIZE) {  
  11.             throw new IllegalArgumentException("corePoolSize: " + corePoolSize);  
  12.         }  
  13.         if ((maximumPoolSize == 0) || (maximumPoolSize < corePoolSize)) {  
  14.             throw new IllegalArgumentException("maximumPoolSize: " + maximumPoolSize);  
  15.         }  
  16.         // Now, we can setup the pool sizes  
  17.         super.setCorePoolSize( corePoolSize );  
  18.         super.setMaximumPoolSize( maximumPoolSize );  
  19.           
  20.         // The queueHandler might be null.  
  21.         if (eventQueueHandler == null) {  
  22.             this.eventQueueHandler = IoEventQueueHandler.NOOP;  
  23.         } else {  
  24.             this.eventQueueHandler = eventQueueHandler;  
  25.         }  
  26. }  

這裏比較意外的是,它竟然用的是SynchronousQueue?! 也就是說,來了一個task,不會被放入Queue中,而是直接送給某個thread。這和一般的threadpoolExecutor是非常不一樣的,因爲一旦thread全用滿了,task就不能再被接受了。後面我們會看到爲什麼使用SynchronousQueue。

 

再看看它的execute函數:

[java] view plaincopy
  1. public void execute(Runnable task) {  
  2.     if (shutdown) {  
  3.         rejectTask(task);  
  4.     }  
  5.     // Check that it's a IoEvent task  
  6.     checkTaskType(task);  
  7.     IoEvent event = (IoEvent) task;  
  8.       
  9.     // Get the associated session  
  10.     IoSession session = event.getSession();  
  11.       
  12.     // Get the session's queue of events  
  13.     SessionTasksQueue sessionTasksQueue = getSessionTasksQueue(session);  
  14.     Queue<Runnable> tasksQueue = sessionTasksQueue.tasksQueue;  
  15.       
  16.     boolean offerSession;  
  17.     boolean offerEvent = eventQueueHandler.accept(this, event);  
  18.       
  19.     if (offerEvent) {  
  20.         // Ok, the message has been accepted  
  21.         synchronized (tasksQueue) {  
  22.             // Inject the event into the executor taskQueue  
  23.             tasksQueue.offer(event);  
  24.               
  25.             if (sessionTasksQueue.processingCompleted) {  
  26.                 sessionTasksQueue.processingCompleted = false;  
  27.                 offerSession = true;  
  28.             } else {  
  29.                 offerSession = false;  
  30.             }  
  31.             //.......  
  32.         }  
  33.     } else {  
  34.         offerSession = false;  
  35.     }  
  36.     if (offerSession) {  
  37.         waitingSessions.offer(session);  
  38.     }  
  39.     addWorkerIfNecessary();  
  40.     //..............  

這裏有幾點需要解釋的:
首先是getSessionTaskQueue函數。從這個函數可以看出,對於每一個session,都創建了一個queue來存儲它的task。也就是說,同一個session的task被放在了同一個queue中。這是非常關鍵的地方,後面會看到,正是這個queue保證了同一個session的task能夠按照順序來執行;
其次是waitingSessions.offer(session)這條語句。waitingSessions是OrderedThreadPoolExecutor的一個私有成員,它也是一個queue: BlockingQueue<IoSession> waitingSessions ...;
這個queue裏面放的是該threadpool所接收到的每個task所對應的Session,並且,如果兩個task對應的是同一個session,那麼這個session只會被放進waitingSessions中一次。waitingSession.offer(session)這條語句就是要將session放進queue。而offerSession這個變量和前面的十幾行代碼就是在判斷task所對應的session是否要放入到queue中;
最後一行代碼addWorkerIfNecessary();字面上很容易理解,就是判斷是否添加worker。可是,worker又是什麼呢?

 

看看Worker這個類:

[java] view plaincopy
  1. private class Worker implements Runnable {  
  2.         private volatile long completedTaskCount;  
  3.         private Thread thread;  
  4.           
  5.         public void run() {  
  6.             thread = Thread.currentThread();  
  7.             try {  
  8.                 for (;;) {  
  9.                     IoSession session = fetchSession();  
  10.                     //..........  
  11.                     try {  
  12.                         if (session != null) {  
  13.                             runTasks(getSessionTasksQueue(session));  
  14.                         }  
  15.                     } finally {  
  16.                         idleWorkers.incrementAndGet();  
  17.                     }  
  18.                 }  
  19.             } finally {  
  20.                 //.......  
  21.             }  
  22.         }  
  23.         private IoSession fetchSession() {  
  24.             //........  
  25.             for (;;) {  
  26.                 try {  
  27.                     try {  
  28.                         session = waitingSessions.poll(waitTime, TimeUnit.MILLISECONDS);  
  29.                         break;  
  30.                     } finally {  
  31.                         //..............  
  32.                     }  
  33.                 } catch (InterruptedException e) {  
  34.                     //........  
  35.                 }  
  36.             }  
  37.             return session;  
  38.         }  
  39.         private void runTasks(SessionTasksQueue sessionTasksQueue) {  
  40.             for (;;) {  
  41.                 //......  
  42.                 runTask(task);  
  43.             }  
  44.         }  
  45.         private void runTask(Runnable task) {  
  46.             beforeExecute(thread, task);  
  47.             boolean ran = false;  
  48.             try {  
  49.                 task.run();  
  50.                 ran = true;  
  51.                 afterExecute(task, null);  
  52.                 completedTaskCount ++;  
  53.             } catch (RuntimeException e) {  
  54.                 if (!ran) {  
  55.                     afterExecute(task, e);  
  56.                 }  
  57.                 throw e;  
  58.             }  
  59.         }  
  60.     }  

在Worker.run()中,一開始就調用fetchSession(),這個函數從WaitingSessions這個queue中拿出一個Session。然後又調用了runTasks,這個函數會將Session中的那個TaskQueue中的每個Task挨個執行一遍。

 

OK,現在OrderedThreadPoolExecutor的整體設計就清晰了:

從外面看上去,OrderedThreadPoolExecutor只是一個thread pool,但本質上,它是有由兩個thread pool拼接而成, 只不過後一個thread pool被隱藏在了類的內部實現中。第一個thread pool中的thread只需要完成很簡單的一個任務,即將接收到的task對應的session添加到waitingSessions中(如果需要的話)。正因爲如此,所以第一個threadpool的queue被設置成了SynchronousQueue。而後一個thread pool中的那些worker(也是一些thread)才真正的執行task。並且,後一個thread pool所能創建的thread的數量也受到了coreSize和MaxSize的限制。所以,整個OrderedThreadPoolExecutor實際上創建了2 * coreSize的thread。


前面的解釋可能有些亂,再重新梳理整個OrderedThreadPoolExecutor的執行流程:
1. 當一個task被接收,前一個thread pool中的某個thread被指定負責處理這個task;
2. thread會找到task所對應的session,將這個task放入該session的TaskQueue中;
3. 如果該session已經被放入了waitingSessions,那麼什麼都不做,否則,將該session放入waitingSessions中;
4. 後一個threadpool中的某一個worker從waitingSessions中將該Session取出;
5. 找到該Session中的TaskQueue,依次執行queue中的task;

 

總結

總的來說,Java的TheadPoolExecutor整體架構設計的很具有擴展性,可以通過繼承改寫來實現不同的各具功能的threadpool,唯一的缺點就是它的調度策略是不能夠改變的,但很多時候一個threadpool的調度策略會對系統性能產生很大的影響。所以,如果ThreadPoolExecutor的調度策略不適合你的話,就只能手工再造個“輪子”了。
另外,如果讀過SOSP01年的“SEDA: An Architecture for Well-Conditioned, Scalable Internet Services”,那麼會發現Java中的ThreadPoolExecutor非常類似於SEDA中的Stage概念。雖然我沒有找到總夠的證據,但是從時間的順序看,java1.5版才加入的ThreadPoolExecutor很可能受到了01年這篇論文的啓發。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章