線程狀態
新創建
剛new出來的Thread還沒有被運行
可運行
一旦調用start 方法,線程處于runnable狀態。一個可運行的線桿可能正在運行也可能沒有運行, 這取決于操作系統給線程提供運行的時間。
被阻塞線程和等待線程
當線程處于被阻塞或等待狀態時, 它暫時不活動。它不運行任何代碼且消耗最少的資源。直到線程調度器重新激活它。細節取決于它是怎樣達到非活動狀態的
- 當一個線程試圖獲取一個內部的對象鎖(而不是javiutiUoncurrent 庫中的鎖),而該鎖被其他線程持有, 則該線程進人阻塞狀態。當所有其他線程釋放該鎖,并且線程調度器允許本線程持有它的時候,該線程將變成非阻塞狀態。
- 當線程等待另一個線程通知調度器一個條件時,它自己進入等待狀態。在調用Object.wait方法或Thread.join方法,或者是等待java,util.concurrent 庫中的Lock 或Condition 時,就會出現這種情況。實際上,被阻塞狀態與等待狀態是有很大不同的。
- 有幾個方法有一個超時參數。調用它們導致線程進人計時等待(timed waiting) 狀態。這一狀態將一直保持到超時期滿或者接收到適當的通知。帶有超時參數的方法有Thread.sleep 和Object.wait、Thread.join、Lock,tryLock以及Condition.await的計時版。
被終止的線程
線程因如下兩個原因之一而被終止:
- 因為run方法正常退出而自然死亡。
- 因為一個沒有捕獲的異常終止了run方法而意外死亡。
同一個線程被 start() 兩次(2019-7-13更新)
Java 的線程是不允許啟動兩次的,第二次調用必然會拋出 IllegalThreadStateException,這是
一種運行時異常,多次調用 start 被認為是編程錯誤。在第二次調用 start() 方法的時候,線程可能處于終止或者其他(非 NEW)狀態,但是不論如何,都是不可以再次啟動的。
創建一個新線程的三種方法
通過Runnable接口創建線程類
- 定義runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
- 創建 Runnable實現類的實例,并依此實例作為Thread的target來創建Thread對象,該Thread對象才是真正的線程對象。
- 調用線程對象的start()方法來啟動該線程。
public class RunnableThreadTest implements Runnable {
private int i;
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " " + i);
if (i == 20) {
RunnableThreadTest rtt = new RunnableThreadTest();
new Thread(rtt, "新線程1").start();
new Thread(rtt, "新線程2").start();
}
}
}
}
繼承Thread類創建線程類
- 定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為執行體。
- 創建Thread子類的實例,即創建了線程對象。
- 調用線程對象的start()方法來啟動該線程。
public class FirstThreadTest extends Thread {
int i = 0;
//重寫run方法,run方法的方法體就是現場執行體
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+ " : " + i);
if (i == 20) {
new FirstThreadTest().start();
new FirstThreadTest().start();
}
}
}
}
Callable 接口
Callable 與 Runable 有兩點不同:
- 可以通過 call() 獲得返回值。
- call() 可以拋出異常
Thread 和 Runnable 的區別
如果一個類繼承 Thread, 則不適合資源共享。但是如果實現了 Runnable 接口的話,則很容易實現資源共享。
總結:
實現 Runnable 接口比繼承 Thread 類具有的優勢:
- 適合多個和相同的程序代碼的線程去共享同一個資源。
- 可以避免 java 中的單繼承的局限性。
- 增加程序的健壯性,實現解耦操作,代碼可以被多個線程共享,代碼和線程獨立。
- 線程池只能放入實現 Runnable或 Callable 類線程,不能直接放入繼承Thread 的類。
中斷線程
沒有任何語言方面的需求要求一個被中斷的線程應該終止。中斷一個線程不過是引起它的注意。被中斷的線程可以決定如何響應中斷。某些線程是如此重要以至于應該處理完異常后, 繼續執行, 而不理會中斷。但是,更普遍的情況是,線程將簡單地將中斷作為一個終止的請求。
線程屬性
線程優先級
每當線程調度器有機會選擇新線程時,它首先選擇具有較高優先級的線程。但是,線程優先級是高度依賴于系統的。當虛擬機依賴于宿主機平臺的線程實現機制時,Java 線程的優先級被映射到宿主機平臺的優先級上,優先級個數也許更多,也許更少。
static void yield( )
導致當前執行線程處于讓步狀態。如果有其他的可運行線程具有至少與此線程同樣高的優先級,那么這些線程接下來會被調度。注意,這是一個靜態方法。
守護線程(2019-7-13更新)
有的時候應用中需要一個長期駐留的服務程序,但是不希望其影響應用退出,就可以將其設置為守護線程,如果 JVM 發現只有守護線程存在時,將結束進程,具體可以參考下面代碼段。注意,必須在線程啟動之前設置。
Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();
同步
為了避免多線程引起的對共享數據的訛誤,必須學習如何同步存取。
競爭條件詳解
當兩個線程試圖同時更新同一個賬戶的時候,這個問題就出現了。假定兩個線程同時執行指令accounts[to] += amount;
問題在于這不是原子操作。該指令可能被處理如下:
- 將
accounts[to]
加載到寄存器。 - 增加
amount
。 - 將結果寫回
accounts[to]
。
現在,假定第1個線程執行步驟1和2, 然后,它被剝奪了運行權。假定第2個線程被喚醒并修改了accounts 數組中的同一項。然后,第1個線程被喚醒并完成其第3步。
這樣,這一動作擦去了第二個線程所做的更新。于是,總金額不再正確。
因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。
鎖同步
鎖Lock
有兩種機制防止代碼塊受并發訪問的干擾。Java語言提供一個 synchronized 關鍵字達到這一目的,并且Java SE 5.0引入了ReentrantLock 類。
public class Bank{
private Lock bankLock = new ReentrantLock0 ;// ReentrantLock implements the Lock interface
public void transfer(int from, intto, int amount){
bankLock.lock();
try
{
System.out.print(Thread.currentThread0);
accounts[from] -= amount;
System.out.printf(" %10.2f from %A to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n",getTotalBalance());
}
finally
{
banklock.unlockO;
}
}
}
重入Lock是一個更強大的工具,他有一個重入功能————當一個線程得到一個對象后,再次請求該對象鎖時是可以再次得到該對象的鎖的。
具體概念就是:自己可以再次獲取自己的內部鎖。因為線程可以重復地獲得已經持有的鎖。鎖保持一個持有計數(holdcount) 來跟蹤對lock 方法的嵌套調用。線程在每一次調用lock 都要調用unlock 來釋放鎖。由于這一特性,被一個鎖保護的代碼可以調用另一個使用相同的鎖的方法。
假定一個線程調用transfer, 在執行結束前被剝奪了運行權。假定第二個線程也調用transfer, 由于第二個線程不能獲得鎖,將在調用lock 方法時被阻塞。它必須等待第一個線程完成transfer 方法的執行之后才能再度被激活。當第一個線程釋放鎖時,那么第二個線程才能開始運行
公平鎖CPU在調度線程的時候是在等待隊列里隨機挑選一個線程,由于這種隨機性所以是無法保證線程先到先得的(synchronized控制的鎖就是這種非公平鎖)。但這樣就會產生饑餓現象,即有些線程(優先級較低的線程)可能永遠也無法獲取CPU的執行權,優先級高的線程會不斷的強制它的資源。那么如何解決饑餓問題呢,這就需要公平鎖了。公平鎖可以保證線程按照時間的先后順序執行,避免饑餓現象的產生。但公平鎖的效率比較低,因為要實現順序執行,需要維護一個有序隊列。
條件對象Condition
假定一個線程已經獲得鎖,將要執行,但是他所需要的條件還沒有滿足(例如在余額不足的情況下取錢),便會造成有鎖卻不執行,其他能夠提供滿足條件的線程(例如存錢)卻只能等待,陷入僵局。
一個鎖對象可以有一個或多個相關的條件對象。你可以用newCondition 方法獲得一個條件對象。習慣上給每一個條件對象命名為可以反映它所表達的條件的名字。例如,在此設置一個條件對象來表達“ 余額充足”條件。
class Bank{
private Condition sufficientFunds;
···
public Bank(){
···
sufficientFunds=bankLock.newCondition();
}
}
如果transfer方法發現余額不足,它調用下面這個方法
sufficientFunds.await();
當前線程現在被阻塞了,并放棄了鎖。我們希望這樣可以使得另一個線程可以進行增加賬戶余額的操作。等待獲得鎖的線程和調用await 方法的線程存在本質上的不同。一旦一個線程調用await方法,它進人該條件的等待集。當鎖可用時,該線程不能馬上解除阻塞。相反,它處于阻塞狀態,直到另一個線程調用同一條件上的signalAll 方法時為止。
synchronized關鍵字
在前面一節中,介紹了如何使用 Lock 和 Condition 對象。在進一步深人之前,總結一下有關鎖和條件的關鍵之處:
- 鎖用來保護代碼片段,任何時刻只能有一個線程執行被保護的代碼。
- 鎖可以管理試圖進入被保護代碼段的線程。
- 鎖可以擁有一個或多個相關的條件對象。
- 每個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程。
Lock 和 Condition 接口為程序設計人員提供了高度的鎖定控制。然而,大多數情況下,并不需要那樣的控制,并且可以使用一種嵌人到Java 語言內部的機制。
如果一個方法用synchronized 關鍵字聲明,那么對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須獲得內部的對象鎖。
換句話說
public synchronized void method()
{
method body
}
其實方法鎖其實鎖的是實例對象,可以等價于
public void method(){
//相當于鎖實例
synchronized(this){
//需要同步的代碼塊
}
}
將靜態方法聲明為synchronized 也是合法的。如果調用這種方法,該方法獲得相關的類對象的內部鎖。例如,如果Bank類有一個靜態同步的方法,那么當該方法被調用時,Bankxlass對象的鎖被鎖住。因此,沒有其他線程可以調用同一個類的這個或任何其他的同步靜態方法。
public static void method(){
//相當于鎖的整個類
synchronized(xxx.class){
//需要同步的代碼塊
}
}
靜態方法同步與非靜態方法同步區別:
- 靜態同步:因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。
- 非靜態同步:鎖住的是該對象,類的其中一個實例,當該對象(僅僅是這一個對象)在不同線程中執行這個同步方法時,線程之間會形成互斥。達到同步效果,但如果不同線程同時對該類的不同對象執行這個同步方法時,則線程之間不會形成互斥,因為他們擁有的是不同的鎖。
內部鎖和條件存在一些局限。包括:
- 不能中斷一個正在試圖獲得鎖的線程。
- 試圖獲得鎖時不能設定超時。
- 每個鎖僅有單一的條件,可能是不夠的
synchronized和ReentrantLock的比較
區別:
- Lock是一個接口,是通過 JDK 來實現的,而 synchronized 是 Java 中的關鍵字,synchronized 是內置的語言實現,是 JVM 實現的;
- synchronized 在發生異常時,會自動釋放線程占有的鎖,因此不會導致死鎖現象發生;而 Lock 在發生異常時,如果沒有主動通過
unLock()
去釋放鎖,則很可能造成死鎖現象,因此使用 Lock 時需要在 finally 塊中釋放鎖; - Lock可以讓等待鎖的線程響應中斷,而 synchronized 卻不行,使用 synchronized 時,等待的線程會一直等待下去,不能夠響應中斷;
- 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
- Lock可以提高多個線程進行讀操作的效率。
兩者在鎖的相關概念上區別:
- 可中斷鎖
顧名思義,就是可以響應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。如果某一線程A正在執行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
lockInterruptibly()
的用法體現了Lock的可中斷性。 - 公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該鎖(并不是絕對的,大體上是這種順序),這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中,synchronized 就是非公平鎖,它無法保證等待的線程獲取鎖的順序。ReentrantLock可以設置成公平鎖。 - 讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作可以并發進行,不需要同步,而寫操作需要同步進行,提高了效率。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。 - 綁定多個條件
一個ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實現一個隱含的條件,如果要和多余一個條件關聯的時候,就不得不額外地添加一個鎖,而ReentrantLock則無須這么做,只需要多次調用new Condition()方法即可。
在新版的 JDK 中, synchronize 也逐漸有了很多優化,除非我們需要用到 ReentrantLock 的高級功能(比如上述幾個鎖),我們盡量選用 synchronize 關鍵詞。
final
還有一種情況可以安全地訪問一個共享域,即這個域聲明為final 時。考慮以下聲明:
final Map<String, Double〉accounts = new HashMap<>() ;
其他線程會在構造函數完成構造之后才看到這個accounts變量。
線程間協作
join
在線程中調用另一個線程的 join() 方法,會將當前線程掛起,而不是忙等待,直到目標線程結束。
wait、notify、notifyall
調用 wait() 使得線程等待某個條件滿足,線程在等待時會被掛起,當其他線程的運行使得這個條件滿足時,其它線程會調用 notify() 或者 notifyAll() 來喚醒掛起的線程。
它們都屬于 Object 的一部分,而不屬于 Thread。
只能用在同步方法或者同步控制塊中使用,否則會在運行時拋出 IllegalMonitorStateException。
使用 wait() 掛起期間,線程會釋放鎖。這是因為,如果沒有釋放鎖,那么其它線程就無法進入對象的同步方法或者同步控制塊中,那么就無法執行 notify() 或者 notifyAll() 來喚醒掛起的線程,造成死鎖。
死鎖
產生條件
- 互斥條件:一個資源每次只能被一個線程使用。
- 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:線程已獲得的資源,在未使用完之前,不能強行剝奪。
- 循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關系。
有3種典型的死鎖類型:
靜態的鎖順序死鎖
a和b兩個方法都需要獲得A鎖和B鎖。一個線程執行a方法且已經獲得了A鎖,在等待B鎖;另一個線程執行了b方法且已經獲得了B鎖,在等待A鎖。這種狀態,就是發生了靜態的鎖順序死鎖。
經典面試問題:寫一個死鎖
class StaticLockOrderDeadLock{
private final Object lockA = new Object();
private final Object lockB = new Object();
public void a(){
synchronized(lockA){
synchronized(lockB){
System.out.println("function a");
}
}
}
public void b(){
synchronized(lockB){
synchronized(lockA){
System.out.println("function b");
}
}
}
}
解決靜態的鎖順序死鎖的方法就是:所有需要多個鎖的線程,都要以相同的順序來獲得鎖。
動態的鎖順序死鎖
動態的鎖順序死鎖是指兩個線程調用同一個方法時,傳入的參數顛倒造成的死鎖。
如下代碼,一個線程調用了transferMoney方法并傳入參數accountA,accountB;另一個線程調用了transferMoney方法并傳入參數accountB,accountA。此時就可能發生在靜態的鎖順序死鎖中存在的問題,即:第一個線程獲得了accountA鎖并等待accountB鎖,第二個線程獲得了accountB鎖并等待accountA鎖。
動態的鎖順序死鎖解決方案如下:使用System.identifyHashCode來定義鎖的順序。確保所有的線程都以相同的順序獲得鎖。
協作對象之間發生的死鎖
有時,死鎖并不會那么明顯,比如兩個相互協作的類之間的死鎖,比如下面的代碼:一個線程調用了Taxi對象的setLocation方法,另一個線程調用了Dispatcher對象的getImage方法。此時可能會發生,第一個線程持有Taxi對象鎖并等待Dispatcher對象鎖,另一個線程持有Dispatcher對象鎖并等待Taxi對象鎖。
上面的代碼中,我們在持有鎖的情況下調用了外部的方法,這是非常危險的(可能發生死鎖)。為了避免這種危險的情況發生,我們使用開放調用。如果調用某個外部方法時不需要持有鎖,我們稱之為開放調用。解決協作對象之間發生的死鎖:需要使用開放調用,即避免在持有鎖的情況下調用外部的方法。
鎖優化
多線程
更多關于Java并發多線程請點擊Java進階學習多線程