1.1 多線程模式
由於本項目使用的 Apache Mina 的框架進行網絡通信。當然其多線程模式也應該在 Mina 框架中體現出來。
爲了理解多線程模式,首先要了解 Mina 的運作方式。
1.1.1 Mina 的通信過程
此次便於解說,假設客戶端也是採用了 Mina 框架來進行,實際上本項目的客戶端只是簡單的使用了 windows 的 Socket 通信。在當前假設下,其通信過程如下圖 16 所示。
圖 16 Apache Mina 的通信過程
本項目只關注服務端,其服務端的通信過程如下:
1 、通過 SocketAcceptor 同客戶端建立連接;
2 、鏈接建立之後 I/O 的讀寫交給了 I/O Processor 線程, I/O Processor 是多線程的;
3 、通過 I/O Processor 讀取的數據經過 IoFilterChain 裏所有配置的 IoFilter , IoFilter 進行消息的過濾,格式的轉換,在這個層面可以制定一些自定義的協議;
4 、最後 IoFilter 將數據交給 Handler 進行業務處理,完成了整個讀取的過程;
5 、寫入過程也是類似,只是剛好倒過來,通過 IoSession.write 寫出數據,然後 Handler 進行寫入的業務處理,處理完成後交給 IoFilterChain ,進行消息過濾和協議的轉換,最後通過 I/O Processor 將數據寫出到 socket 通道。
1.1.2 Mina 的多級線程池
服務端的一個簡化過程如下圖 17 所示:
圖 17 多級線程池
在基於 SocketAcceptor 的應用程序中,運行過程中 Mina 框架本身會有兩種類型的線程在運行,一種是在 SocketAcceptor 中創建的用於監聽並接收來自客戶端請求的線程,還有一類線程是處理客戶端與服務器端 I/O 的線程,即 Processor 的處理線程。後面還有第三類,就是過濾器層的線程。這個多級的概念後面你會體會到, Acceptor 是一級線程池,而 Processor 的線程池主要通過 ExecutorFileter 進行添加,當然可以添加多個層次的線程池。下面逐層進行講解。
1 、第一類線程池:
當調用 SocketAcceptor 的 bind 方法時,默認會創建一個名稱前綴爲 SocketAcceptor 的線程,該線程負責監聽來自客戶端的請求,如果接收到客戶端的請求,它僅僅是爲處理這個請求做好準備,而把具體處理請求以及 I/O 的任務代理給 SocketIoProcessor ,讓它去處理請求。這個準備過程主要是爲接受到的請求創建一個 IoSession ,並構建出 IoFilter 鏈,然後把準備好的數據以及來自客戶端的請求交給 SocketIoProcessor 處理。通常 這類線程會針對每一次的 bind 調用創建一個新的線程。
請注意 “通常 ” 二字,使用這兩個字說明上述的行爲不是絕對的。的確這不是絕對的,這取決於 SocketAcceptor 的字段 executor 的實現,可以通過構造方法來設置字段 executor 的值, executor 字段的類型爲 java.util.concurrent.Executor 。 mina 默認是用 org.apache.mina.util.NewThreadExecutor ,它會爲每一個提交的任務創建一個新的線程。因爲在 bind 方法中它會把實現監聽客戶端請求任務的 Runnable 提交到 executor 中去執行。注意千萬不要使用一個不創建新線程而是在原線程中執行的 Executor ,這會使程序無法監聽客戶端的請求,因爲程序中的唯一線程會被 Selector.get() 方法所阻塞,詳情可以查看 SocketAcceptor 類的源代碼。
第二類線程池:
當 SocketAcceptor 收到了來自客戶端的請求,它就會把此請求丟給 SocketIoProcessor 去處理,這會創建名稱以 SocketAcceptorIoProcessor 爲前綴的線程, mina 框架在這類線程中處理 I/O 發佈並處理事件。這一類線程的數量可以通過 SocketAcceptor 的構造函數來設置。具體的值可以根據應用的具體需求來決定。
作爲 I/O 真正處理的線程,存在於服務器端和客戶端,用來處理 I/O 的讀寫操作,線程的數量是可以配置的,默認最大數量是 CPU 個數 +1 。
在服務器端中,在創建 SocketAcceptor 的時候指定 ProcessorCount 。
SocketAcceptor acceptor =
new SocketAcceptor(Runtime.getRuntime().availableProcessors() + 1, Executors.newCachedThreadPool());
NioProcessor 雖然是多線程,但是對與一個連接的時候業務處理只會使用一個線程進行處理( Processor 線程對於一個客戶端連接只使用一個線程 NioProcessor-n )如果 handler 的業務比較耗時,會導致 NioProcessor 線程堵塞 ,在 2 個客戶端同時連接上來的時候會創建第 2 個(前提是第 1 個 NioProcessor 正在忙),創建的最大數量由 Acceptor 構造方法的時候指定。如果:一個客戶端連接同服務器端有很多通信,並且 I/O 的開銷不大,但是 Handler 處理的業務時間比較長,那麼需要採用獨立的線程模式,在 FilterChain 的最後增加一個 ExecutorFitler ,這個就是第三類線程池了。
第三類線程池:
上述的兩類線程是 mina 框架本身所創建的,如果你的應用每次處理請求的時間較長而又希望應用能夠有較好的響應性,那麼最好是把處理業務邏輯的任務放到一個新的線程中去執行,而不是在 mina 框架創建的線程中去執行。 mina 框架本身提供了一個過濾器 ExecutorFilter 來完成這樣的任務,它會把在此之後的過濾器以及 IoHandler 中處理業務邏輯的代碼放到一個新的線程中去執行。當 mina 框架中的第二類線程執行完此過濾器後就會立即返回,可以用於處理新的請求。如果不想使用此過濾器,還可以設置 mina 的線程模型來達到相同的效果,其實線程模型也是使用 ExecutorFilter 實現的。但需要注意的是,在 mina 2.0 版本中已經廢棄了線程模型。
使用類這三次線程池,性能可以得到保證了,在本項目中,主要配置了第二類線程池和第三類線程池。第二類線程池在新建 NioAcceptor 對象(以建立 TCP 監聽服務器爲例)時候,在其構造函數中體現,而這個數值需要多次測試來設定,其測試方法在國外網站有完整表述,請自行 Google ;第三類線程池設定在 Apache Mina 的過濾器層,一般而言只需要設置一層,設置在最消耗時間的業務前面,如比較複雜的解碼,或者是數據庫訪問模塊。
關於共享線程池問題, Apache Mina 有個官方說法:你可以想讓 IoServices 和 ExecutorFilters 共享一個線程池,而不是一家一個。這個是不禁止的,但是會出現很多問題,在這種情況下,除非你爲 IoServices 建立一個緩衝線程池。 本人尚未考究。