線程狀態(tài)

新創(chuàng)建
剛new出來的Thread還沒有被運(yùn)行
可運(yùn)行
一旦調(diào)用start 方法,線程處于runnable狀態(tài)。一個(gè)可運(yùn)行的線桿可能正在運(yùn)行也可能沒有運(yùn)行, 這取決于操作系統(tǒng)給線程提供運(yùn)行的時(shí)間。
被阻塞線程和等待線程
當(dāng)線程處于被阻塞或等待狀態(tài)時(shí), 它暫時(shí)不活動。它不運(yùn)行任何代碼且消耗最少的資源。直到線程調(diào)度器重新激活它。細(xì)節(jié)取決于它是怎樣達(dá)到非活動狀態(tài)的
- 當(dāng)一個(gè)線程試圖獲取一個(gè)內(nèi)部的對象鎖(而不是javiutiUoncurrent 庫中的鎖),而該鎖被其他線程持有, 則該線程進(jìn)人阻塞狀態(tài)。當(dāng)所有其他線程釋放該鎖,并且線程調(diào)度器允許本線程持有它的時(shí)候,該線程將變成非阻塞狀態(tài)。
- 當(dāng)線程等待另一個(gè)線程通知調(diào)度器一個(gè)條件時(shí),它自己進(jìn)入等待狀態(tài)。在調(diào)用Object.wait方法或Thread.join方法,或者是等待java,util.concurrent 庫中的Lock 或Condition 時(shí),就會出現(xiàn)這種情況。實(shí)際上,被阻塞狀態(tài)與等待狀態(tài)是有很大不同的。
- 有幾個(gè)方法有一個(gè)超時(shí)參數(shù)。調(diào)用它們導(dǎo)致線程進(jìn)人計(jì)時(shí)等待(timed waiting) 狀態(tài)。這一狀態(tài)將一直保持到超時(shí)期滿或者接收到適當(dāng)?shù)耐ㄖ?。帶有超時(shí)參數(shù)的方法有Thread.sleep 和Object.wait、Thread.join、Lock,tryLock以及Condition.await的計(jì)時(shí)版。
被終止的線程
線程因如下兩個(gè)原因之一而被終止:
- 因?yàn)閞un方法正常退出而自然死亡。
- 因?yàn)橐粋€(gè)沒有捕獲的異常終止了run方法而意外死亡。
同一個(gè)線程被 start() 兩次(2019-7-13更新)
Java 的線程是不允許啟動兩次的,第二次調(diào)用必然會拋出 IllegalThreadStateException,這是
一種運(yùn)行時(shí)異常,多次調(diào)用 start 被認(rèn)為是編程錯(cuò)誤。在第二次調(diào)用 start() 方法的時(shí)候,線程可能處于終止或者其他(非 NEW)狀態(tài),但是不論如何,都是不可以再次啟動的。

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