多任務(wù)(multitasking):在同一時刻運行多個程序的能力桑孩。
并發(fā)執(zhí)行的進程數(shù)目并不是由CPU數(shù)目制約的。操作系統(tǒng)將CPU的時間片分配給每一個進程,給人并發(fā)處理的感覺线得。
多線程程序在較低的層次上擴展了多任務(wù)的概念:一個程序同時執(zhí)行多個任務(wù)。通常徐伐,每一個任務(wù)稱為線程(thread)贯钩,它是線程控制的簡稱『浅浚可以同時運行一個以上線程的程序稱為多線程程序(multithreaded)魏保。
多進程與多線程有哪些區(qū)別呢?本質(zhì)的區(qū)別在于每個進程擁有自己的一整套變量摸屠,而線程則共享數(shù)據(jù)谓罗。
14.1 什么是線程
調(diào)用Thread.sleep不會創(chuàng)建一個新線程,sleep是Thread類的靜態(tài)方法季二,用于暫停當(dāng)前線程的活動檩咱。
14.2 中斷線程
當(dāng)線程的run方法執(zhí)行方法體中最后一條語句后,并經(jīng)由執(zhí)行return語句返回時胯舷,或者出現(xiàn)了在方法中沒有捕獲的異常時刻蚯,線程將終止。
注釋:Interrupted方法是一個靜態(tài)方法桑嘶,它監(jiān)測當(dāng)前的線程是否被中斷炊汹。而且,調(diào)用interrupted方法會清除該線程的中斷狀態(tài)逃顶。另一方面讨便,isInterrupted方法是一個實例方法,可用來檢驗是否有線程被中斷以政。調(diào)用這個方法不會改變中斷的狀態(tài)霸褒。
線程狀態(tài)
線程可以有如下6種狀態(tài):
- New(新創(chuàng)建)
- Runnable(可運行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed waiting(計時等待)
- Terminated(被終止)
新創(chuàng)建線程
當(dāng)用new操作符創(chuàng)建一個新線程時,如new Thread(r)盈蛮,該線程還沒有開始運行废菱。這意味著它的狀態(tài)是new。當(dāng)一個線程處于新創(chuàng)建狀態(tài)時,程序還沒有開始運行線程中的代碼殊轴。在線程運行之前還有一些基礎(chǔ)工作要做衰倦。
可運行線程
一旦調(diào)用start方法,線程處于runnable狀態(tài)梳凛。一個可運行的線程可能正在運行也可能沒有運行耿币,這取決于操作系統(tǒng)給線程提供運行的時間。搶占式調(diào)度系統(tǒng)給每一個可運行線程一個時間片來執(zhí)行任務(wù)韧拒。當(dāng)時間片用完淹接,操作系統(tǒng)剝奪該線程的運行權(quán),并給另一個線程運行機會∨岩纾現(xiàn)在所有的桌面以及服務(wù)器操作系統(tǒng)都是用搶占式調(diào)度塑悼。
記住,在任何給定時刻楷掉,一個可運行的線程可能正在運行也可能沒有運行(這就是為什么將這個狀態(tài)稱為可運行而不是運行)厢蒜。
被阻塞線程和等待線程
當(dāng)線程處于被阻塞或等待狀態(tài)時,它暫時不活動烹植。它不運行任何代碼且消耗最少的資源斑鸦。直到線程調(diào)度器重新激活它。細節(jié)取決于它是怎樣達到非活動狀態(tài)的草雕。
當(dāng)一個線程試圖獲取一個內(nèi)部的對象鎖(而不是java.util.concurrent庫中的鎖)巷屿,而該鎖被其他線程持有,則該線程進入阻塞狀態(tài)
當(dāng)線程等待另一個線程通知調(diào)度器一個條件時墩虹,它自己進入等待狀態(tài)嘱巾。在調(diào)用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent庫中的Lock或Condition時诫钓,就會出現(xiàn)這種情況旬昭。實際上,被阻塞狀態(tài)與等待狀態(tài)是很大不同的菌湃。
有幾個方法有一個超時參數(shù)问拘。調(diào)用它們導(dǎo)致線程進入計時等待(timed waiting)狀態(tài)。這一狀態(tài)將一直保持到超時期滿或者接收到適當(dāng)?shù)耐ㄖ逅в谐瑫r參數(shù)的方法有Thread.sleep和Object.wait场梆、Thread.join、Lock.tryLock以及Condition.await的計時版纯路。
被終止的線程
線程因如下兩個原因之一而被終止:
- 因為run方法正常退出而自然死亡;
- 因為一個沒有捕獲的異常終止了run方法而意外死亡寞忿。
14.4 線程的屬性
同步
14.5 同步
synchronized關(guān)鍵字
總結(jié)一些有關(guān)鎖和條件的關(guān)鍵之處:
- 鎖用來保護代碼片段驰唬,任何時刻只能有一個線程執(zhí)行被保護的代碼。
- 鎖可以管理試圖進入被保護代碼段的線程。
- 鎖可以擁有一個或多個相關(guān)的條件對象叫编。
- 每個條件對象管理那些已經(jīng)進入被保護的代碼段但還不能運行的線程辖佣。
Lock和Condition接口為程序設(shè)計人員提供了高度的鎖定控制。然而搓逾,大多數(shù)情況下卷谈,并不需要那樣的控制,并且可以使用一種嵌入到Java語言內(nèi)部的機制霞篡。從1.0版開始世蔗,Java中的每一個對象都有一個內(nèi)部鎖。如果一個方法用了synchronized關(guān)鍵字聲明朗兵,那么對象的鎖將保護整個方法污淋。也就是說,要調(diào)用該方法余掖,線程必須獲得內(nèi)部的對象鎖寸爆。
換句話說,
public synchronized void method(){
method body;
}
等價于
public void method(){
this.intrinsicLock.lock();
try{
method body;
}
finally{this.intrinsicLock.unlock();}
}
將靜態(tài)方法聲明為synchronized也是合法的盐欺。如果調(diào)用這種方法赁豆,該方法獲得相關(guān)的類對象的內(nèi)部鎖。例如冗美,如果Bank類有一個靜態(tài)同步的方法魔种,那么當(dāng)該方法被調(diào)用時,Bank.class對象的鎖被鎖住墩衙。因此务嫡,沒有其他線程可以調(diào)用同一個類的這個或任何其他的同步靜態(tài)方法。
內(nèi)部鎖和條件存在一些局限漆改。包括:
- 不能中斷一個正在試圖獲得鎖的線程心铃。
- 試圖獲得鎖時不能設(shè)定超時。
- 每個鎖僅有單一的條件挫剑,可能時不夠的去扣。
在代碼中應(yīng)該使用哪一種?Lock和Condition對象還是同步方法樊破?下面是一些建議:
- 最好即不適用Lock/Condition也不適用synchronized關(guān)鍵字愉棱。在許多情況下你可以使用java.util.concurrent包中的一種機制,它會為你處理所有的加鎖哲戚。
- 如果synchronized關(guān)鍵字適合你的程序奔滑,那么請盡量使用它,這樣可以減少編寫的代碼數(shù)量顺少,減少出錯的幾率朋其。
- 如果特別需要Lock/Condition結(jié)構(gòu)提供的獨有特性時王浴,才使用Lock/Condition。
同步阻塞
正如剛剛討論的梅猿,每一個Java對象有一個鎖氓辣。線程可以通過調(diào)用同步方法獲得鎖。還有另一種機制可以獲得鎖袱蚓,通過進入一個同步阻塞钞啸。當(dāng)線程進入如下形式的阻塞:
synchronized(obj)
{
critical setion
}
于是它獲得obj的鎖。
有時會發(fā)現(xiàn)“特殊的”鎖喇潘,例如:
public class Bank
{
private double[] accounts;
private Object lock = new Object();
...
public void transfer(int from,int to, int amount){
synchronized(lock){
accounts[from] -= amount;
accounts[to] += amount;
}
System.out.println(...);
}
}
在此体斩,lock對象被創(chuàng)建僅僅是用來使用每個Java對象持有的鎖。
有時程序員使用一個對象的鎖來實現(xiàn)額外的原子操作响蓉,實際上稱為客戶端鎖定(clientside locking)硕勿。
監(jiān)視器概念
鎖和條件是線程同步的強大工具,但是枫甲,嚴(yán)格地講源武,它們不是面向?qū)ο蟮摹6嗄陙硐牖茫芯咳藛T努力尋找一種方法粱栖,可以在不需要程序員考慮如何加鎖的情況下,就可以保證多線程的安全性脏毯。
用Java的術(shù)語來講闹究,監(jiān)視器具有如下特性:
- 監(jiān)視器是只包含私有域的類。
- 每個監(jiān)視器類的對象有一個相關(guān)的鎖食店。
- 使用該鎖對所有的方法進行加鎖渣淤。換句話說,如果客戶端調(diào)用obj.method()吉嫩,那么obj對象的鎖是在方法調(diào)用開始時自動獲得价认,并且當(dāng)方法返回時自動釋放該鎖。因為所有的域是私有的自娩,這樣的安排可以確保一個線程在對對像操作時用踩,沒有其他線程能訪問該域。
- 該鎖可以有任意多個相關(guān)條件忙迁。
Java設(shè)計者以不是很精確的方式采用了監(jiān)視器概念脐彩,Java中的每一個對象有一個內(nèi)部鎖和內(nèi)部的條件。如果一個方法用synchronized關(guān)鍵字聲明姊扔,那么惠奸,它表現(xiàn)的就是一個監(jiān)視器方法。通過調(diào)用wait/notifyAll/notify來訪問條件變量恰梢。
然而佛南,在下述的3個方面Java對象不同于監(jiān)視器证九,從而使得線程的安全性下降:
- 域不要求必須是private。
- 方法不要求必須是synchronized共虑。
- 內(nèi)部鎖對客戶是可用的。
volatile域
有時呀页,僅僅為了讀寫一個或兩個實例域就使用同步妈拌,顯得開銷過大了。畢竟蓬蝶,什么地方能出錯呢尘分?遺憾的是,使用現(xiàn)代的處理器與編譯器丸氛,出錯的可能性很大培愁。
- 多處理器的計算機能夠暫時在寄存器或本地內(nèi)存緩沖區(qū)中保存內(nèi)存中的值。結(jié)果是缓窜,運行在不同處理器上的線程可能在同一個內(nèi)存位置取到不同的值定续。
- 編譯器可以改變指令執(zhí)行的順序以使吞吐量最大化。這種順序上的變化不會改變代碼語義禾锤,但是編譯器假定內(nèi)存的值僅僅在代碼中有顯示的修改指令時才會改變私股。然而,內(nèi)存的值可以被另一個線程改變恩掷!
如果你使用鎖來保護可以被多個線程訪問的代碼倡鲸,那么可以不考慮這種問題。編譯器被要求通過在必要的時候刷新本地緩存來保護鎖的效應(yīng)黄娘,并且不能不正當(dāng)?shù)刂匦屡判蛑噶睢?/p>
注釋:Brian Goetz給出了下述“同步格言”:“如果向一個變量寫入值峭状,而這個變量接下來可能會被另一個線程讀取,或者逼争,從一個變量讀值优床,而這個變量可能時之前被另一個線程寫入的,此時必須使用同步”氮凝。
volatile關(guān)鍵字為實例域的同步訪問提供了一種免鎖機制羔巢。如果聲明一個域為volatile,那么編譯器和虛擬機就知道該域是可能被另一個線程并發(fā)更新的罩阵。
final變量
除非使用鎖或volatile修飾符竿秆,否則無法從多個線程安全地讀取一個域。
還有一種情況可以安全地訪問一個共享域稿壁,即這個域聲明為final時幽钢。
原子性
java.util.concurrent.atomic包中有很多類使用了很高效地機器級指令(而不是鎖)來保證其他操作的原子性。
原子性(Atomic)
一個事務(wù)包含多個操作傅是,這些操作要么全部執(zhí)行匪燕,要么全都不執(zhí)行蕾羊。實現(xiàn)事務(wù)的原子性,要支持回滾操作帽驯,在某個操作失敗后龟再,回滾到事務(wù)執(zhí)行之前的狀態(tài)。
回滾實際上是一個比較高層抽象的概念尼变,大多數(shù)DB在實現(xiàn)事務(wù)時利凑,是在事務(wù)操作的數(shù)據(jù)快照上進行的(比如,MVCC)嫌术,并不修改實際的數(shù)據(jù)哀澈,如果有錯并不會提交,所以很自然的支持回滾度气。
而在其他支持簡單事務(wù)的系統(tǒng)中割按,不會在快照上更新,而直接操作實際數(shù)據(jù)磷籍∈嗜伲可以先預(yù)演一邊所有要執(zhí)行的操作,如果失敗則這些操作不會被執(zhí)行择示,通過這種方式很簡單的實現(xiàn)了原子性束凑。
如果有大量線程要訪問相同的原子值,性能會大幅下降栅盲,因為樂觀更新需要太多次重試汪诉。Java SE 8提供了LongAdder和LongAccumulator類來解決這個問題。LongAdder包括多個變量(加數(shù))谈秫,其總和為當(dāng)前值扒寄。可以有多個線程更新不同的加數(shù)拟烫,線程個數(shù)增加時會自動提供新的加數(shù)该编。通常情況下,只有當(dāng)所有工作都完成之后才需要總和的值硕淑,對于這種情況课竣,這種方法會很高效。性能會有顯著的提升置媳。
死鎖
考慮下面的情況:
賬戶1:¥200
賬戶2:¥300
線程1:從賬戶1轉(zhuǎn)移¥300到賬戶2
線程2:從賬戶2轉(zhuǎn)移¥400到賬戶1
如圖所示于樟,線程1和線程2都被阻塞了。因為賬戶1以及賬戶2中余額都不足以進入轉(zhuǎn)賬拇囊,兩個線程都無法執(zhí)行下去迂曲。
有可能會因為每一個線程要等待更多的錢款存入而導(dǎo)致所有線程都被阻塞。這樣的狀態(tài)稱為死鎖(deadlock)寥袭。
導(dǎo)致死鎖的另一種途徑是讓第i個線程負責(zé)向第i個賬戶存錢路捧,而不是從第i個賬戶取錢关霸。這樣一來,有可能將所有的線程都集中到一個賬戶上杰扫,每一個線程都試圖從這個賬戶中取出大于該賬戶余額的錢队寇。
線程局部變量
有時可能要避免共享變量,使用ThreadLocal輔助類為各個線程提供各自的實例章姓。
例如英上,SimpleDateFormat類不是線程安全的。假設(shè)有一個靜態(tài)變量:
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
如果兩個線程都執(zhí)行以下操作:
String dateStamp = dateFormat.format(new Date());
結(jié)果可能很混亂啤覆,因為dateFormat使用的內(nèi)部數(shù)據(jù)結(jié)構(gòu)可能被并發(fā)的訪問所破壞。當(dāng)然可以使用同步惭聂,但開銷很大窗声;或者也可以在需要時構(gòu)造一個局部SimpleDateFormat對象,不過這也太浪費了辜纲。
要為每個線程構(gòu)造一個實例笨觅,可以使用以下代碼:
public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(()->new SimpleDateFormat(
"yyyy-MM-dd"));
要訪問具體的格式化方法,可以調(diào)用:
String dateStamp = dataFormat.get().format(new Date());
在一個給定線程中首次調(diào)用get時耕腾,會調(diào)用initialValue方法见剩。在此之后,get方法會返回屬于當(dāng)前線程的那個實例扫俺。
在多個線程中生成隨機數(shù)也存在類似的問題苍苞。java.util.Random類是線程安全的。但是如果多個線程需要等待一個共享的隨機數(shù)生成器狼纬,這會很低效羹呵。
可以使用ThreadLocal輔助類為各個線程提供一個單獨的生成器,不過Java SE 7還另外提供了一個便利類疗琉。只需要做一下調(diào)用:
int random = ThreadLocalRandom.current().nextInt(upperBound);
ThreadLocalRandom.current()調(diào)用會返回特定于當(dāng)前線程的Random類實例冈欢。
讀/寫鎖
如果很多線程從一個數(shù)據(jù)結(jié)構(gòu)讀取數(shù)據(jù)而很少線程修改其中數(shù)據(jù)的話,后者是十分有用的盈简。在這種情況下凑耻,允許對讀者線程共享訪問是合適的。當(dāng)然柠贤,寫者線程依然必須是互斥訪問的香浩。
使用讀/寫鎖的必要步驟:
- 構(gòu)造一個ReentrantReadWriteLock對象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
- 抽取讀鎖和寫鎖:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
- 對所有的獲取方法加讀鎖:
public double getTotalBalance(){
? readLock.lock();
? try{...}
? finally { readLock.unlock();}
}
- 對所有的修改方法加寫鎖:
public void transfer(...){
? writeLock.lock();
? try{...}
? finally {writeLock.unlock();}
}
為什么棄用stop和suspend方法
stop、suspend和resume方法已經(jīng)棄用种吸。stop方法天生就不安全弃衍,經(jīng)驗證明suspend方法會經(jīng)藏導(dǎo)致死鎖。
注釋:一些作者聲稱stop方法被棄用是因為它會導(dǎo)致對象被一個已停止的線程永遠鎖定坚俗。但是镜盯,這一說法是錯誤的岸裙。從技術(shù)上講,被停止的線程通過拋出ThreadDeath異常退出所有它調(diào)用的同步方法速缆。結(jié)果是降允,該線程釋放了它持有的內(nèi)部對象鎖。