java 網絡編程(2.3)-----------採用線程池多線程的Socket 通信

 吃了個泡麪,看了會盤龍,然後放了一首歌《17歲那年的雨季》(隨機的 - -)

 

好了 。接着來說多線程吧,爲什麼把兩騙分開來呢,可能是這篇比較難的

 

兩個類

EchoServer.java

 

 

好吧,這個代碼跟前面的基本上沒什麼區別,除了那句

threadPool.execute(new Handler(socket)); //把與客戶通信的任務交給線程池

 

就是說爲每個客戶鏈接分配一個線程不同,當然咯,我們採用線程池了嘛。

 

ThreadPool.java

 

哎,這個類就難多了,看了哥好久,對多線程還是沒能把握呀。。

 

 

那麼只好說下2個小時裏面我乾的事了:

 

首先運行EchoServer.java

結果:

當前線程池大小爲4
我要啓動所有線程咯
當前調用線程0
當前調用線程1
當前調用線程2
當前調用線程3
服務器啓動
當前隊列中沒有任務,我睡覺了
當前隊列中沒有任務,我睡覺了
當前隊列中沒有任務,我睡覺了
當前隊列中沒有任務,我睡覺了

這個輸出說明什麼呢,說明了線程池的執行順序,其實他是這麼執行的首先傳入poolSize大小,自然就會調用ThreadPool.java的構造函數了,然後構造函數創建相應的線程個數,而後,每個線程都會去getTask()...

這個是什麼呢?就是到任務隊列去尋找有沒有任務,這裏任務隊列是LinkedList<Runnable> workQueue

 

---------------------------------------------------------------------------------------------------------------------

好。。分下段
---------------------------------------------------------------------------------------------------------------------

這裏要題下,因爲是線程,所以就會不段運行(我理解爲死循環可以麼?),那麼實際上在getTask()函數裏

有個wait(),我一開始不明白,後來我把他去掉後發現他會不斷輸出“我要睡覺了”,而其實本來是隻輸出4個

我要睡覺了,因爲只有四個線程麼。。。。

 

--------------------------------------------------------------------------------------------------------------------

好了,一開始代碼的運行順序我知道了,然後我就要測試他是怎麼執行任務的了,於是打開cmd,

輸入telnet localhost 6500

 結果:

已經將一個任務房到工作隊列中了
......正在喚醒一個線程,給我去工作
New connection accepted /127.0.0.1:3438

 

觀看代碼裏的輸出信息就可以發現,當客戶端連接時,首先是EchServer.java

執行代碼 threadPool.execute(new Handler(socket)); 

 

然後在 ThreadPool.java中把該任務放到workQueue中,再喚醒一個線程(我不知道是不是隨即喚醒的)

 

被喚醒的線程就會繼續檢查workQueue中是否有任務,這個時候自然會發現有任務咯。。然後就會把任務隊列中

 

第一個任務移除。。。

 

 

值得一題的是(其實不值一提) 線程池中的線程數目是4個,我無聊得用5個客戶端去訪問6500端口,然後用第5個客戶端

發送消息--------------結果自然是沒有反應咯。。等到關閉第一個客戶端,消息框中就會輸出剛纔發送的內容。。也說明了

任務隊列是有序的(好像是P話 - -)。。

 

 

好了。。這個就是我們後來2個多小時的結果。。。阿門

 

然後記下還沒看懂的(其實還有很多沒看懂):

 

疑問:

 

首先super()其實並不是很懂了。。只是簡單理解爲調用父類構造函數,哎,上課沒怎麼聽,不過這次主要是爲了理解

網絡編程,應該關係不大,以後再看吧,

 

然後在ThreadPool裏的一個setDaemon(true);也不清楚啥意思,算了。丟掉

 

當然還會發現,在ThreadPool裏的join()與close()要這麼調用呢???

 

然後最最最不懂的就是synchronized了,貼段百度解釋吧:

 

synchronized 方法控制對類成員變量的訪問:每個類實例對應一把鎖,每個 synchronized 方法都必須獲得調用該方法的類實例的鎖方能執行,否則所屬線程阻塞,方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。這種機制確保了同一時刻對於每一個類實例,其所有聲明爲 synchronized 的成員函數中至多隻有一個處於可執行狀態(因爲至多隻有一個能夠獲得該類實例對應的鎖),從而有效避免了類成員變量的訪問衝突(只要所有可能訪問類成員變量的方法均被聲明爲 synchronized)。

---------------------------------------------------------------------------------------------------------------------

 

下面是摘自網上對ThreadPool.java的理解:

 

 

 在ThreadPool類中定義了一個LinkedList類型的workQueue成員變量,它表示工作隊列,用來存放線程池要執行的任務,每個 任務都是Runnable實例。ThreadPool類的客戶程序(利用ThreadPool來執行任務的程序)只要調用ThreadPool類的 execute (Runnable task)方法,就能向線程池提交任務。在ThreadPool類的execute()方法中,先判斷線程池是否已 經關閉。如果線程池已經關閉,就不再接收任務,否則就把任務加入到工作隊列中,並且喚醒正在等待任務的工作線程。

 

在ThreadPool類的構造方法中,會創建並啓動若干工作線程,工作線程的數目由構造方法的參數 poolSize決定。WorkThread類表示工作線程,它是ThreadPool類的內部類。工作線程從工作隊列中取出一個任務,接着執行該任務, 然後再從工作隊列中取出下一個任務並執行它,如此反覆。

工作線程從工作隊列中取任務的操作是由ThreadPool類的getTask()方法實現的,它的處理邏輯如下:

◆如果隊列爲空並且線程池已關閉,那就返回null,表示已經沒有任務可以執行了;

◆如果隊列爲空並且線程池沒有關閉,那就在此等待,直到其他線程將其喚醒或者中斷;

◆如果隊列中有任務,就取出第一個任務並將其返回。

線程池的join()和close()方法都可用來關閉線程池。join()方法確保在關閉線程池之前,工作線程把隊列中的所有任務都執行完。而close()方法則立即清空隊列,並且中斷所有的工作線程。

ThreadPool類是ThreadGroup類的子類。ThreadGroup類表示線程組,它提供了一些管理線程組中線程的方法。例如, interrupt()方法相當於調用線程組中所有活着的線程的interrupt()方法。線程池中的所有工作線程都加入到當前ThreadPool對 象表示的線程組中。

ThreadPool類在close()方法中調用了interrupt()方以上interrupt()方法用於中斷所有的工作線程。interrupt()方法會對工作線程造成以下影響:

◆如果此時一個工作線程正在ThreadPool的getTask()方法中因爲執行wait()方法而阻塞,則會拋出InterruptedException;

◆如果此時一個工作線程正在執行一個任務,並且這個任務不會被阻塞,那麼這個工作線程會正常執行完任務,但是在執行下一輪while (!isInterrupted()) {…}循環時,由於isInterrupted()方法返回true,因此退出while循環。

 

 

 

 3.6.4  使用線程池的注意事項(其實我還沒看過^^)

雖然線程池能大大提高服務器的併發性能,但使用它也會存在一定風險。與所有多線程應用程序一樣,用線程池構建的應用程序容易產生各種併發問題,如對 共享資源的競爭和死鎖。此外,如果線程池本身的實現不健壯,或者沒有合理地使用線程池,還容易導致與線程池有關的死鎖、系統資源不足和線程泄漏等問題。

1.死鎖

任何多線程應用程序都有死鎖風險。造成死鎖的最簡單的情形是,線程A持有對象X的鎖,並且在等待對象Y的鎖,而線程B持有對象Y的鎖,並且在等待對象X的鎖。線程A與線程B都不釋放自己持有的鎖,並且等待對方的鎖,這就導致兩個線程永遠等待下去,死鎖就這樣產生了。

雖然任何多線程程序都有死鎖的風險,但線程池還會導致另外一種死鎖。在這種情形下,假定線程池中的所有工作線程都在執行各自任務時被阻塞,它們都在 等待某個任務A的執行結果。而任務A依然在工作隊列中,由於沒有空閒線程,使得任務A一直不能被執行。這使得線程池中的所有工作線程都永遠阻塞下去,死鎖 就這樣產生了。

2.系統資源不足

如果線程池中的線程數目非常多,這些線程會消耗包括內存和其他系統資源在內的大量資源,從而嚴重影響系統性能。

3.併發錯誤

線程池的工作隊列依靠wait()和notify()方法來使工作線程及時取得任務,但這兩個方法都難於使用。

如果編碼不正確,可能會丟失通知,導致工作線程一直保持空閒狀態,無視工作隊列中需要處理的任務。因此使用這些方法時,必須格外小心,即便是專家也 可能在這方面出錯。最好使用現有的、比較成熟的線程池。例如,直接使用java.util.concurrent包中的線程池類。

4.線程泄漏

使用線程池的一個嚴重風險是線程泄漏。對於工作線程數目固定的線程池,如果工作線程在執行任務時拋出RuntimeException 或Error,並 且這些異常或錯誤沒有被捕獲,那麼這個工作線程就會異常終止,使得線程池永久失去了一個工作線程。如果所有的工作線程都異常終止,線程池就最終變爲空,沒 有任何可用的工作線程來處理任務。

導致線程泄漏的另一種情形是,工作線程在執行一個任務時被阻塞,如等待用戶的輸入數據,但是由於用戶一直不輸入數據(可能是因爲用戶走開了),導致 這個工作線程一直被阻塞。這樣的工作線程名存實亡,它實際上不執行任何任務了。假如線程池中所有的工作線程都處於這樣的阻塞狀態,那麼線程池就無法處理新 加入的任務了。

5.任務過載

當工作隊列中有大量排隊等候執行的任務時,這些任務本身可能會消耗太多的系統資源而引起系統資源缺乏。

綜上所述,線程池可能會帶來種種風險,爲了儘可能避免它們,使用線程池時需要遵循以下原則。

(1)如果任務A在執行過程中需要同步等待任務B的執行結果,那麼任務A不適合加入到線程池的工作隊列中。如果把像任務A一樣的需要等待其他任務執行結果的任務加入到工作隊列中,可能會導致線程池的死鎖。

(2)如果執行某個任務時可能會阻塞,並且是長時間的阻塞,則應該設定超時時間,避免工作線程永久的阻塞下去而導致線程泄漏。在服務器程序中,當線程等待客戶連接,或者等待客戶發送的數據時,都可能會阻塞。可以通過以下方式設定超時時間:

◆調用ServerSocket的setSoTimeout(int timeout)方法,設定等待客戶連接的超時時間,參見本章3.5.1節(SO_TIMEOUT選項);

◆對於每個與客戶連接的Socket,調用該Socket的setSoTimeout(int timeout)方法,設定等待客戶發送數據的超時時間,參見本書第2章的2.5.3節(SO_TIMEOUT選項)。

(3)瞭解任務的特點,分析任務是執行經常會阻塞的I/O操作,還是執行一直不會阻塞的運算操作。前者時斷時續地佔用CPU,而後者對CPU具有更高的利用率。預計完成任務大概需要多長時間?是短時間任務還是長時間任務?

根據任務的特點,對任務進行分類,然後把不同類型的任務分別加入到不同線程池的工作隊列中,這樣可以根據任務的特點,分別調整每個線程池。

(4)調整線程池的大小。線程池的最佳大小主要取決於系統的可用CPU的數目,以及工作隊列中任務的特點。假如在一個具有 N 個CPU的系統上只 有一個工作隊列,並且其中全部是運算性質(不會阻塞)的任務,那麼當線程池具有 N 或 N+1 個工作線程時,一般會獲得最大的 CPU 利用率。

如果工作隊列中包含會執行I/O操作並常常阻塞的任務,則要讓線程池的大小超過可用CPU的數目,因爲並不是所有工作線程都一直在工作。選擇一個典 型的任務,然後估計在執行這個任務的過程中,等待時間(WT)與實際佔用CPU進行運算的時間(ST)之間的比例WT/ST。對於一個具有N個CPU的系 統,需要設置大約N×(1+WT/ST)個線程來保證CPU得到充分利用。

當然,CPU利用率不是調整線程池大小過程中唯一要考慮的事項。隨着線程池中工作線程數目的增長,還會碰到內存或者其他系統資源的限制,如套接字、打開的文件句柄或數據庫連接數目等。要保證多線程消耗的系統資源在系統的承載範圍之內。

(5)避免任務過載。服務器應根據系統的承載能力,限制客戶併發連接的數目。當客戶併發連接的數目超過了限制值,服務器可以拒絕連接請求,並友好地告知客戶:服務器正忙,請稍後再試。


文章出處:http://www.diybl.com/course/3_program/java/javaxl/2008229/102026_5.html

 

發佈了8 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章