1. 線程的狀態
java
中線程狀態分爲 6 種
-
New
初始: 新創建了一個線程對象, 但是還沒有調用start()
方法. -
Runnable
運行:Java
線程中將就緒(ready)
和運行中(running)
兩種狀態都成爲 "運行".
線程對象創建後, 其他線程(比如main
線程) 調用了該對象的start()
方法, 該狀態的線程位於可運行線程池中, 等待被線程調度選中, 獲取 CPU 的使用權, 此時就處於就緒狀態(ready)
. 就緒狀態的線程在獲得 CPU 時間片後變爲運行中(running)
狀態. -
Blocked
阻塞: 表示線程阻塞於鎖 -
Waiting
等待: 進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)後纔可變爲Runnable
運行狀態. -
Timed_Waiting
超時等待: 這個狀態不同於Waiting
, 它可以在指定的時間後自行變爲Runnable
運行狀態. -
Terminated
終止: 表示該線程已經執行完畢.
2. 線程的啓動方式
-
xxx extends Thread
然後調用xxx.start
-
xxx implements Runnable
然後交給Thread
運行. - 區別:
Thread
纔是Java
中對線程唯一抽象,Runnable
只是對任務(業務邏輯)的抽象.Thread
可以接受任意一個Runnable
的實例並執行.
start()
方法只是將一個線程進入就緒隊列等待分配 CPU, 分到 CPU 後纔會去調用實現的run()
方法.start()
方法不能重複調用, 否則拋出異常.
run
方法是業務邏輯實現的地方, 可以重複執行, 也可以被單獨調用.
2. 線程的終止
- 線程自然終止
線程的中止要麼是
run
執行完成了, 要麼是拋出了一個未處理的異常,從而導致線程提前結束.
- 使用
stop()
方法
不建議使用,
stop()
方法在終結一個線程時不會保證線程資源的正常釋放, 通常是沒有給線程完成資源釋放工作的機會, 因此會導致程序可能工作在不確定的狀態下.
- 使用
interrupt
中斷方式.
線程安全的終止是其他線程通過調用某個線程的
interrupt()
方法對其進行中斷操作.
中斷好比對其他線程打了個招呼. 不代表線程 A 會立即停止自己的工作. 同樣的 A 線程也可以不理會這種中斷請求. 因爲java
中的線程是協作式的, 不是搶佔式的. 線程內部可以通過調用isInterrupted()
方法看返回是否爲true/false
來判斷是否被中斷, 也可以調用靜態方法Thread.interrupted()
來進行判斷是否中斷.
interrupted()
與isInterrupted()
不同的是interrupted()
方法如果發現當前線程被中斷, 則會清除中斷標誌, 也就是如果第一次調用是true
, 再次調用返回的就是false
, 因爲之前的中斷狀態被清除了.
線程 A 調用了
wait
系列的函數,join
方法或者sleep
方法而被阻塞掛起, 這時候若是線程 B 調用線程 A 的interrupt()
方法, 線程 A 會在調用這些方法的地方拋出InterruptedException
異常後會立即將線程 A 的中斷標誌位清楚, 即重新設置爲false
.
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
if(!Thread.currentThread().isInterrupted()){
System.out.println("thread name:" + Thread.currentThread().getName());
}else {
return;
}
}
}
});
thread.start();
thread.interrupt();
System.out.println("main is over");
}
不建議自定義一個標誌位來終止線程的運行, 因爲
run
方法裏有阻塞調用時會無法很快檢測到取消標誌, 線程必須從阻塞調用返回後, 纔會檢查這個取消標誌. 這種情況下使用中斷會更好. 因爲一般的阻塞方法, 如sleep()
等本身就支持中斷的檢查.
注意: 處於死鎖狀態的線程無法被中斷.
3. Join
將指定的線程加入到當前線程, 可以將兩個交替執行的線程合併爲順序執行, 比如在線程 B 中調用了線程 A 的 join()
方法, 那麼會直到線程 A 執行完畢後, 纔會繼續執行線程 B.
4. synchronized
關鍵字synchronized
可以修飾方法或者以同步塊的形式來進行使用, 它主要確保多個線程在同一個時刻, 只能有一個線程處於方法或者同步塊中, 它爆炸了線程對變量訪問的可見性和排他性. 又稱爲內置鎖機制.
對象鎖是用於對象實例方法或者一個對象實例上的, 類鎖是用於類的靜態方法或者一個類的
class
對象上的. 類的對象實例可以有很多個, 但是每個類只有一個class
對象, 所以不同對象實例的對象鎖是互不干擾的, 但是每個類只有一個類鎖.
類鎖只是一個概念上的東西, 並不是真實存在的, 類鎖其實鎖的是每個類對應的
class
對象. 類鎖與對象鎖之間互不干擾.
5. 等待/通知機制
- wait / notify / notifyAll
是指一個線程 A 調用了 wait()
方法進入到等待狀態, 而另外一個線程 B 調用了對 notify()
或者 notifyAll()
方法. 那麼線程 A 在接受到通知後就會被喚醒, 進而執行後續操作.
- notify
喚醒一個等待的線程, 喚醒的前提是該線程獲取到了鎖. 沒有獲得鎖就會重新進入waiting
狀態. - nofifyAll
喚醒所有等待狀態的線程. - wait
調用該方法的線程將會進入到waiting
狀態, 只有等待另外線程的喚醒或者被中斷纔會返回. 注意: 調用wait()
方法後會釋放對象的鎖.
在調用
wait / notify / notifyAll
之前, 線程必須要獲得該對象的對象級鎖, 即只能在同步方法或同步塊中調用wait / notify / notifyAll
方法. 進入wait()
方法後, 當前線程就會釋放鎖.
儘可能使用
notifyAll()
, 因爲notify()
只會喚醒一個線程, 我們無法確保被喚醒的這個線程一定就是我們需要喚醒的線程.
6. 死鎖
是指兩個或者兩個以上的進程在執行過程中, 由於競爭資源或者彼此通信而造成的一種阻塞的現象, 若無外力作用, 它們都將無法推進下去. 此時稱系統處於死鎖狀態.
死鎖是必然發生在多操作者(M>=2) 的情況下, 增多多個資源(N>=2, 並且 N<=M). 纔會發生這種情況.
例:
A 線程和 B 線程都想操作蘋果和香蕉這兩個對象, 但是線程 A 先拿到了蘋果, 線程 B 拿到了香蕉, 線程 A 就一直在等香蕉, 線程 B 就一直在等蘋果, 結果就產生了死鎖.
死鎖的危害
- 線程不工作了, 但是整個進程又是活着的.
- 沒有任何異常信息供我們檢查.
- 一旦程序發生了死鎖, 是沒有任何辦法恢復的, 只有重啓程序.
死鎖的發生的四個必要條件
互斥
指線程對分配到的資源進行排他性使用. 即在一段時間內某資源只由一個線程佔用. 如果此時還有其他線程請求資源, 則請求者只能等待, 直至佔有資源的進程使用完畢釋放.請求和保持
指線程已經保持至少一個資源, 但又提出了新的資源使用請求, 而該新的資源已經被其他線程佔有, 此時請求線程阻塞, 但是又對已持有的資源保持不釋放.不剝奪
指線程已獲得的資源, 在未使用完之前, 不能被剝奪, 只能在使用完時由自己釋放.環路等待
指在發生死鎖時, 必然存在一個線程資源的環形鏈, 即線程集合{T0, T1, T2...Tn}中的 T0 正在等待一個 T1 佔用的資源, 而 T1 呢又在等在 T2 佔用的資源, Tn又在等待 T0 佔用的資源.
死鎖的解除/預防/避免
理解了死鎖的原因, 尤其是產生死鎖的四個必要條件, 那麼只要打破這四個必要條件中的任何一個, 就能夠預防死鎖的發生.
例如
- 當一個線程獨佔有一份資源後, 又申請了一個獨佔資源而無法滿足的情況下, 就退出原有的資源.
- 採用資源預先分配策略, 即線程運行前申請全部資源, 滿足則運行, 不然就等待. 這樣就不會有佔有且申請的情況發生.
- 實現資源有序分配策略, 對所有資源實現分類編號, 所有線程只能採用按序號遞增的形式申請資源.
7. 活鎖
兩個線程在嘗試獲得鎖的過程中, 發生線程之間的謙讓, 不斷髮生同一個線程總拿到同一把鎖, 在嘗試獲得另外一把鎖的時候因爲拿不到, 而將本來已持有的鎖釋放的過程.
解決方案
- 每個線程休眠隨機數, 錯開拿鎖的時間.
8. 線程飢餓
低優先級的線程, 總拿不到執行時間.