Java線程基礎
線程狀態
在Thread.java類文件中,有一個state靜態枚舉內部類,預定義了Thread的狀態。
State Name | Dec |
---|---|
NEW | 線程未開啟 |
RUNNABLE | 線程可運行狀態,一個可運行的線程在JVM中運行,但有可能因為處理器等操作系統的原因處于等待 |
BLOCKED | 阻塞狀態,處于阻塞狀態的線程,等待監視器的鎖。例如調用Object.wait()后,線程處于阻塞,等待獲取監視器的鎖進入同步代碼塊、方法或重新進入同步代碼塊、方法。 |
WAITING | 等待狀態,一個等待狀態的線程等待另一個線程去執行特定的操作。例如一個線程使用了Thread.join(),該線程等待指定線程終結;一個線程調用了一個對象的wait(),等待另一個線程調用這個對象的notify()/notifyAll()喚醒等待的線程。 |
TIMED_WAITING | 線程等待指定的時間。 |
TERMINATED | 線程被終止,線程完成任務。 |
BLOCKED&WAITING
在文檔中介紹,BOLCKED狀態主要指多線程程序運行時,多個線程進入臨界區,線程互相排斥,等待鎖的獲取與釋放。而WAITING泛指處于等待狀態的線程等待另一個線程去執行任務。
WAITING狀態是因為Object.wait(),Thread.join()等方法(無參重載方法,不需要指定時間)造成的。而BLOCKED主要是因為同步中鎖的獲取與釋放以及IO流阻塞方法。
WAITING&TIMED_WAITING
WAITING與TIMED_WAITHING的區別在于WAITING需要手動喚醒,例如調用Object.notify()/Object.notifyAll();而TIMED_WAITING狀態是線程等待一定的時間。
造成TIMED_WAITING狀態的原因可能是調用了以下方法:
- Thread.sleep()
- Object.wait()的指定時間重載方法
- Thread.join()的指定時間重載方法
- 等等
jstack
使用JDK內置工具jstack可以查看線程處于什么狀態。一般的jstack查看命令jstack pid
。而pid是指Java虛擬機的進程id,可以通過jps
命令來查看。
了解了線程狀態,以及他們的原因和解決辦法,遇到問題時就有頭緒解決。例如線程處于阻塞狀態,可能是因為臨界區的鎖沒有釋放等。
更多JDK內置工具可以參考 jstack命令(Java Stack Trace)
參考
Lock
一般實現同步,都是給代碼臨界區加上synchronized,變成同步語句或同步方法。這樣保證線程排斥,同一時刻只允許一個線程進入代碼臨界區。實際上,執行同步方法或語句之前,線程隱式的給實例(或類)加了一個鎖。在Java中可以顯示加鎖。
鎖的作用
Lock實際上創建的是相互排斥的鎖。當線程執行臨界區時,首先通過lock.lock()
獲取鎖,執行完畢后通過lock.unlock()
釋放鎖,讓其他獲取到鎖的線程有機會執行。由于是排斥的鎖,所以同一時刻只有一個線程可以進入代碼臨界區。
代碼編寫過程中,一般使用try-finally,在finally塊中釋放鎖。
線程協作
由于線程的調度完全由CPU分配(線程共享CPU時間切片),導致程序無法按照開發人員設計運行。所以線程之間的協作尤為重要。
可以通過Lock.newCondition()
創建一個條件對象(Condition)。線程間的協作需要通信,通過Condition對象線程間相互通信,指定線程在條件下該做何種操作。
創建條件實例后,可以通過以下方法實現線程通信:
- await(),使線程進入WAITING狀態,直到執行條件發生,通過手動喚醒;
- signal(),條件滿足,喚醒該條件等待的一條線程;
- signalAll(),條件滿足,喚醒該條件等待的所有線程。
注意,使用條件的方法前,必須已經獲取了鎖,否則會拋出異常。使用await()進入等待的線程,必須使用喚醒方法,否則一直處于等待(WAITING)狀態。
await(),可以讓當前鎖定臨界區的線程釋放該條件的鎖,允許其他線程進入。而且當使用signal()/signalAll()喚醒等待線程后,執行先前使用await()的下一行代碼。
監視器
鎖和條件都是Java 5中新增的內容,在此之前,線程之間的通信是通過對象內置的監視器編程實現的。
監視器是一個相互排斥且具備同步能力的對象。監視器中的一個時間點上,只能有一個線程執行一個方法。
線程通過獲取監視器上的鎖進入監視器,并且通過釋放鎖退出監視器。而這個鎖是通過線程執行synchronized方法塊或方法來獲取的。也就是說監視器對象可以是任意對象,但必須是synchronized鎖住的對象。
通過調用監視器對象的wait()來釋放鎖并且進入等待狀態,讓監視器對象中的其它線程有機會執行任務;當條件合適時,另一個線程中調用監視器的notify()/notifyAll()來通知一個或所有等待線程重新獲取鎖并且恢復執行。
當監視器調用wait()時,阻塞當前線程,并且釋放鎖。當監視器調用notify()/notifyAll()時,通知該線程啟動,并且可以獲取鎖。
Object的wait()、notify()、notifyAll()類似于Condition的await()、signal()、signalAll()。
死鎖
又是兩個線程或多個線程需要在幾個共享對象上獲取鎖,這就有可能導致死鎖。
例如,線程a獲取了對象A的鎖,并且在等待對象B的鎖;而此時線程b獲取了對象B的鎖,在等待對象A的鎖。這樣一來線程a,b都無法獲取到需要的鎖繼續執行任務(沒有線程釋放所需的鎖),導致死鎖。
解決辦法是通過資源排序技術。該技術指定給對象上鎖順序,避免死鎖。
例如指定對象A,B上鎖的順序必須是A在前,那么線程a,b就不會死鎖,因為線程b必須先獲取對象A的鎖才可以獲取對象B的鎖,否則一直處于BLOCKED。
一個線程從一個共享對象獲取鎖后,此時有另一個線程獲取該對象的鎖時,由于鎖的互斥,第二個線程無法執行。
中斷線程
終止線程有兩個原因:
- 線程的run()執行到最后一條語句,并執行return語句返回時,自然終止;
- 線程的run()執行時遇到一個沒有捕獲的異常意外終止。
沒有可以強制終止線程的方法(stop()被廢棄),但是可以使用interrupt()來請求終止線程,當調用它時,線程的中斷狀態被置位。這是每一個線程都有一個boolean標志位,且應該不時地去檢查這個標志位,以判斷線程是否被中斷。
沒有強制線程終止的方法,但是通過interrupt()方法,可以將中斷狀態標志位置位,然后去檢查它,來判斷線程是否被中斷(實際上線程未終止,可以根據標志位去執行特定的操作)。
當一個線程在調用interrupt()后,被置于阻塞狀態(阻塞調用后進入阻塞狀態,sleep或wait等方法的執行),就會拋出InterruptedException異常,中斷阻塞調用。
線程的中斷非終止。任何語言都沒有需求要求一個被中斷的線程應該被終止(置線程于TERMINATED狀態)。
中斷線程是引起線程的注意,被中斷的線程決定如何響應中斷,保證了線程在安全的時候停止。普遍的處理是線程將中斷作為一個終止請求。
public void run() {
try {
...
while(!Thread.currentThread().isInterrupted() //check interrput boolean
&& more work to do) {
do more work
}
}catch(InterruptedException) {
//Thread was interrupted during sleep or wait
}finally {
cleanup,if required //do something for terminated
}
//exiting the run method terminates the thread
}
中斷方法
-
void interrupt()
,請求中斷,中斷標志位置位。 -
static boolean interrupted()
,靜態方法,檢測當前線程是否被中斷(其實代碼就是檢測中斷標志位),且清楚該線程的中斷標志位值。如果被中斷,返回TRUE,否則返回FALSE。 -
boolean isInterrupted()
,用來檢測是否有線程被中斷,但是不會清楚中斷狀態。如果被中斷,返回TRUE,否則返回FALSE。
GUI線程
Java的GUI事件處理和繪制任務在一個event dispatch thread中執行。這是因為大多數GUI方法都是非線程安全的,如果從多線程中執行這些任務會造成沖突。
線程安全,如果一段代碼在多線程程序中沒有導致競爭狀態,則稱這樣的代碼是線程安全的。
可以通過javax.swing.Utilities類中的靜態方法,在event dispatch thread中執行任務。
- Utilities.invokeLater(),該方法無須等待任務執行完畢就返回。
- Utilities.invokeAndWait(),該方法必須等待任務執行完畢后返回,導致方法阻塞。
信號量
信號量可以用來限制訪問共享資源的線程數量。在訪問資源之前,線程從信號量獲取許可,此時總的許可減一;在訪問完資源后,線程必須把許可釋放還給信號量。
當信號量的許可總數為1時,可以實現同步。
Thread.sleep()&Object.wait()
在以前的JDK版本中還有一個suspend()
實例方法,用于阻塞線程。但是它極其容易導致死鎖,擁有鎖的線程被掛起后,在恢復之前(resume())無法釋放鎖。如果一個線程調用另一個線程suspend(),且該線程需要獲取同一個鎖時,就造成了死鎖。
Thread.sleep()也不會釋放鎖,直接阻塞線程。但是由于它是靜態方法,所以阻塞的是當前線程,不會造成死鎖。
Condition、監視器的阻塞線程的方法可以釋放鎖,當等待中的線程被喚醒時又有機會重新獲取鎖。
所以他們的主要區別有:
- 它們來自不同的類,同時sleep()屬于靜態方法,使當前線程進入TIMED_WAITING。
- sleep()不會釋放鎖,而wait()會。
- sleep()可以在線程中的任意地方調用,而wait()必須在同步方法或同步語句中調用。
- sleep()必須捕獲InterruptedExpection,而wait()不需要。
鎖的可重入
鎖的可重入是指,當一個線程試圖請求獲取已經持有的鎖時,這個請求可以成功。重入的一個重要作用是防止死鎖:
public class Father {
public synchronized void doSomething(){ ......
}
}
public class Child extends Father {
public synchronized void doSomething(){ ......
super.doSomething();
}
}
當一個線程調用Child實例的doSomething()時,首先請求獲取該實例的鎖,在執行super.doSomething()時再次請求獲取實例的鎖。
如果沒有重入,當調用super.doSomething()時無法再次獲取Child實例鎖,線程會一直阻塞下去,造成死鎖。而鎖的重入就避免了這種死鎖。
但是當退出super.soSomething()時,線程并未釋放鎖,只有退出Child實例的doSomething()時,才會釋放鎖,讓其他線程進入。