線程的基本知識

java多線程的基本知識

線程在一定條件下,狀態會發生變化。線程一共有以下幾種狀態:

1、新建狀態(New):新創建了一個線程對象。

2、就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於“可運行線程池”中,變得可運行,只等待獲取CPU的使用權即在就緒狀態的進程除CPU之外,其它的運行所需資源都已全部獲得。

3、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。

4、阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。

阻塞的情況分三種:

(1)、等待阻塞:運行的線程執行wait()方法,該線程會釋放佔用的所有資源,JVM會把該線程放入“等待池”中。進入這個狀態後,是不能自動喚醒的,必須依靠其他線程調用notify()或notifyAll()方法才能被喚醒,

(2)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入“鎖池”中。

(3)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

5、死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

線程變化的狀態轉換圖如下:


注:拿到對象的鎖標記,即爲獲得了對該對象(臨界區)的使用權限。即該線程獲得了運行所需的資源,進入“就緒狀態”,只需獲得CPU,就可以運行。因爲當調用wait()後,線程會釋放掉它所佔有的“鎖標誌”,所以線程只有在此獲取資源才能進入就緒狀態。
下面小小的作下解釋: 
1、線程的實現有兩種方式,一是繼承Thread類,二是實現Runnable接口,但不管怎樣,  當我們new了這個對象後,線程就進入了初始狀態; 
2、當該對象調用了start()方法,就進入就緒狀態; 
3、進入就緒後,當該對象被操作系統選中,獲得CPU時間片就會進入運行狀態; 
4、進入運行狀態後情況就比較複雜了 
    4.1、run()方法或main()方法結束後,線程就進入終止狀態; 
    4.2、當線程調用了自身的sleep()方法或其他線程的join()方法,進程讓出CPU,然後就會進入阻塞狀態(該狀態既停止當前線程,但並不釋放所佔有的資源即調用sleep ()函數後,線程不會釋放它的“鎖標誌”。)。當sleep()結束或join()結束後,該線程進入可運行狀態,繼續等待OS分配CPU時間片。典型地,sleep()被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓線程阻塞一段時間後重新測試,直到條件滿足爲止。
    4.3、線程調用了yield()方法,意思是放棄當前獲得的CPU時間片,回到就緒狀態,這時與其他進程處於同等競爭狀態,OS有可能會接着又讓這個進程進入運行狀態; 調用 yield() 的效果等價於調度程序認爲該線程已執行了足夠的時間片從而需要轉到另一個線程。yield()只是使當前線程重新回到可執行狀態,所以執行yield()的線程有可能在進入到可執行狀態後馬上又被執行。
   4.4、當線程剛進入可運行狀態(注意,還沒運行),發現將要調用的資源被synchroniza(同步),獲取不到鎖標記,將會立即進入鎖池狀態,等待獲取鎖標記(這時的鎖池裏也許已經有了其他線程在等待獲取鎖標記,這時它們處於隊列狀態,既先到先得),一旦線程獲得鎖標記後,就轉入就緒狀態,等待OS分配CPU時間片;

4.5. suspend() 和 resume()方法:兩個方法配套使用,suspend()使得線程進入阻塞狀態,並且不會自動恢復,必須其對應的resume()被調用,才能使得線程重新進入可執行狀態。典型地,suspend()和 resume() 被用在等待另一個線程產生的結果的情形:測試發現結果還沒有產生後,讓線程阻塞,另一個線程產生了結果後,調用 resume()使其恢復。 
   4.6、wait()和 notify() 方法:當線程調用wait()方法後會進入等待隊列(進入這個狀態會釋放所佔有的所有資源,與阻塞狀態不同),進入這個狀態後,是不能自動喚醒的,必須依靠其他線程調用notify()或notifyAll()方法才能被喚醒(由於notify()只是喚醒一個線程,但我們由不能確定具體喚醒的是哪一個線程,也許我們需要喚醒的線程不能夠被喚醒,因此在實際使用時,一般都用notifyAll()方法,喚醒有所線程),線程被喚醒後會進入鎖池,等待獲取鎖標記。 

wait() 使得線程進入阻塞狀態,它有兩種形式:

一種允許指定以毫秒爲單位的一段時間作爲參數;另一種沒有參數。前者當對應的 notify()被調用或者超出指定時間時線程重新進入可執行狀態即就緒狀態,後者則必須對應的 notify()被調用。當調用wait()後,線程會釋放掉它所佔有的“鎖標誌”從而使線程所在對象中的其它synchronized數據可被別的線程使用。waite()和notify()因爲會對對象的“鎖標誌”進行操作,所以它們必須在synchronized函數或synchronizedblock中進行調用。如果在non-synchronized函數或non-synchronizedblock中進行調用,雖然能編譯通過,但在運行時會發生IllegalMonitorStateException的異常。

 

注意區別:初看起來wait() 和 notify() 方法與suspend()和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的suspend()及其它所有方法在線程阻塞時都不會釋放佔用的鎖(如果佔用了的話),而wait() 和 notify() 這一對方法則相反。

上述的核心區別導致了一系列的細節上的區別

首先,前面敘述的所有方法都隸屬於 Thread類,但是wait() 和 notify() 方法這一對卻直接隸屬於 Object 類也就是說,所有對象都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因爲這一對方法阻塞時要釋放佔用的鎖,而鎖是任何對象都具有的,調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。而調用任意對象的notify()方法則導致因調用該對象的 wait()方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。

其次,前面敘述的所有方法都可在任何位置調用,但是wait() 和 notify() 方法這一對方法卻必須在 synchronized 方法或塊中調用,理由也很簡單,只有在synchronized方法或塊中當前線程才佔有鎖,纔有鎖可以釋放。同樣的道理,調用這一對方法的對象上的鎖必須爲當前線程所擁有,這樣纔有鎖可以釋放。因此,這一對方法調用必須放置在這樣的 synchronized方法或塊中,該方法或塊的上鎖對象就是調用這一對方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現IllegalMonitorStateException異常。

wait() 和 notify()方法的上述特性決定了它們經常和synchronized方法或塊一起使用,將它們和操作系統的進程間通信機制作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於操作系統原語的功能,它們的執行不會受到多線程機制的干擾,而這一對方法則相當於 block和wake up 原語(這一對方法均聲明爲 synchronized)。它們的結合使得我們可以實現操作系統上一系列精妙的進程間通信的算法(如信號量算法),並用於解決各種複雜的線程間通信問題。

關於 wait() 和 notify() 方法最後再說明兩點:

第一:調用notify() 方法導致解除阻塞的線程是從因調用該對象的 wait()方法而阻塞的線程中隨機選取的,我們無法預料哪一個線程將會被選擇,所以編程時要特別小心,避免因這種不確定性而產生問題。

第二:除了notify(),還有一個方法 notifyAll()也可起到類似作用,唯一的區別在於,調用 notifyAll()方法將把因調用該對象的 wait()方法而阻塞的所有線程一次性全部解除阻塞。當然,只有獲得鎖的那一個線程才能進入可執行狀態。

談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend()方法和不指定超時期限的wait()方法的調用都可能產生死鎖。遺憾的是,Java並不在語言級別上支持死鎖的避免,我們在編程中必須小心地避免死鎖。


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