吃了個泡麪,看了會盤龍,然後放了一首歌《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