我以為,學(xué)習(xí)java編程钮孵,關(guān)鍵在于三點骂倘,OOP,并發(fā)和JVM巴席。最后兩點其實是聯(lián)系比較緊密的历涝,并且是屬于java語言特有的屬性。之前在學(xué)習(xí)java的時候?qū)Σl(fā)的理解比較粗淺漾唉,利用這一段時間荧库,進(jìn)一步的學(xué)習(xí)了java并發(fā)的定義,原理和例子赵刑,寫篇文章總結(jié)一下分衫。
之前看過一篇關(guān)于Java多線程編程的文章,里面有一句話特別好般此,說的是多線程編程看上去很深奧蚪战,在學(xué)習(xí)概念牵现,理解和設(shè)計并發(fā)程序上很容易出錯。但是
從根本上來看邀桑,所謂的多線程編程瞎疼,不過是JVM或者說當(dāng)前的計算機(jī)體系結(jié)構(gòu)無法處理好多線程下資源競爭的情況而人為加上的一些處理方法。這樣的方法導(dǎo)致在實現(xiàn)相同功能時候會產(chǎn)生很多復(fù)雜的壁畸,讓開發(fā)者難以理解或者設(shè)計缺陷贼急,僅此而已。
有了這樣的前提捏萍,我們可以認(rèn)為太抓,多線程編程無非是為了更好的壓榨CPU的性能,人為設(shè)計出來的補(bǔ)償機(jī)制令杈。不過在宏觀上走敌,我們可以藐視這樣的機(jī)制,但是在工作里这揣,還是不能避免要用到它悔常,而且還要用好它。
先說說一些基本定義给赞,這些定義在無數(shù)的博客和書籍上都有解釋机打,假定讀者已經(jīng)有所了解,這里只是枚舉出最簡潔的幾點片迅。
進(jìn)程與線程的區(qū)別(面試題常見題残邀,但是一般問出這個問題的面試官要么是真沒實際開發(fā)多線程程序的經(jīng)驗,要么是對面試者比較失望柑蛇,問個簡單的理論問題湊個數(shù)芥挣。。耻台。)
-
線程的狀態(tài)
-
Java中多線程的實現(xiàn)
- Interface: Runnable, Callable, Future, ExecutorService
- Class: Thread, FutureTask
-
說明
- Thread空免,實現(xiàn)類,start()方法將線程變?yōu)榭蛇\行狀態(tài)盆耽,在運行態(tài)的時候調(diào)用定義的run()方法蹋砚。不過一般不會有人用定義子類的方式定義一個線程
- Runnable, 接口摄杂,通常實現(xiàn)這個接口坝咐,然后作為構(gòu)造參數(shù)新建一個線程實例
- Callable, Java 1.5, java.util.concurrent, 與runnable類似,call()方法可以返回線程運行的狀態(tài)析恢,并且可以拋出異常
- FutureTask墨坚,包裝器,處于thread和callable的中間映挂,它通過接受Callable來創(chuàng)建泽篮,同時實現(xiàn)了Future和Runnable接口盗尸,可以檢查線程的狀態(tài)
Java語法中的多線程機(jī)制
synchronized 關(guān)鍵字
synchronized關(guān)鍵字是Java 1.0就有的語法元素。在Java中咪辱,所有的object實例(class也是一種object)都可以作為多線程環(huán)境下得競爭資源振劳,所以每個oject上都有一個鎖的標(biāo)記椎组,在執(zhí)行關(guān)鍵代碼的時候油狂,對非null的object加上synchronize關(guān)鍵字,標(biāo)記一個代碼塊寸癌,可以自動對某個對象加解鎖专筷。
舉個小栗子:
public class SynchronizedTest {
private int count = 0;
public synchronized void increaseCount() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5000; i++) {
synchronizedTest.increaseCount();
}
}
}, "Thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i < 5000; i++) {
synchronizedTest.increaseCount();
}
}
}, "Thread2");
thread1.start();
thread2.start();
Thread.currentThread().join(2000);//main thread waiting for sub-threads perform
System.out.println("count is: " + synchronizedTest.getCount());
}
}
啟兩個線程,并發(fā)的對一個變量做自增操作蒸苇,這個操作在synchronized標(biāo)識下變?yōu)榇械倪^程磷蛹,最后輸出10000,如果不加synchronized溪烤,結(jié)果會小于10000味咳。
synchronized可能產(chǎn)生死鎖
public class DeadLock {
private Object a = new Object();
private Object b = new Object();
public static void main(String[] args) throws InterruptedException {
DeadLock deadLock = new DeadLock();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (deadLock.a) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (deadLock.b) {
System.out.println("Thread 1 enter");
}
}
}
}, "Thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (deadLock.b) {
synchronized (deadLock.a) {
System.out.println("Thread 2 enter");
}
}
}
}, "Thread2");
thread1.start();
thread2.start();
}
}
以上,thread1在請求到a的鎖之后會帶著鎖睡一會兒檬嘀,然后再請求b的鎖槽驶,但是這是b的鎖已經(jīng)在thread2手里了,同時thread2還在請求a的鎖鸳兽,變成了循環(huán)等待并且是無限等待掂铐,于是產(chǎn)生了死鎖。運行結(jié)果是兩個線程都無限的等待下去揍异。要想解這樣的死鎖全陨,可以在競爭資源上加上是否被鎖的標(biāo)記位,然后引入等待超時的機(jī)制衷掷,使得有一方在請求資源超時之后做出讓步辱姨,把手上已有的鎖也釋放了,改變循環(huán)等待的狀態(tài)戚嗅。但是雨涛,即使有了超時機(jī)制,也需要注意有過度退讓的情況存在渡处,形象的說镜悉,好比在一個只能容納一個人通過的窄巷里,你和另一個人迎面走來医瘫,然后你們發(fā)現(xiàn)這樣誰也過不去侣肄,于是都高風(fēng)亮節(jié)的往后退出巷子,然后等待一會兒醇份,又很默契的一起走了進(jìn)去稼锅,結(jié)果是悲劇的又發(fā)生了死鎖的情況吼具,而且會持續(xù)下去。這就需要兩個線程之間需要知道對方的情況而不是盲目的退讓矩距。
Synchronize的可重入性
所以可重入性拗盒,是指在某個線程得到某個對象的鎖之后,不需要額外申請該對象的鎖也可以進(jìn)入關(guān)鍵代碼塊锥债。
Synchronized的JVM層實現(xiàn)
Synchronized在設(shè)計之初被實現(xiàn)為一種重量鎖陡蝇,每次做互斥系統(tǒng)開銷很大。在Java 1.6之后做了優(yōu)化調(diào)整哮肚,加入鎖升級的機(jī)制去減小每次鎖的開銷登夫。在JVM中,每個object都有一個header允趟,保存object的一些信息恼策,普通對象頭的長度為兩個字,數(shù)組對象頭的長度為三個字(JVM內(nèi)存字長等于虛擬機(jī)位數(shù)潮剪,32位虛擬機(jī)即32位一字涣楷,64位亦然),其中有兩個bit位記錄了對象的鎖類型:
偏向鎖
鎖對象第一次被線程獲取的時候抗碰,虛擬機(jī)把對象頭的status設(shè)置為"01"狮斗,偏向鎖狀態(tài),當(dāng)發(fā)生鎖重入時改含,只需要檢查MarkValue中的ThreadID是否與當(dāng)前線程ID相同即可情龄,相同即可直接重入。偏向鎖的釋放不需要做任何事情捍壤,這也就意味著加過偏向鎖的MarkValue會一直保留偏向鎖的狀態(tài)骤视,因此即便同一個線程持續(xù)不斷地加鎖解鎖,也是沒有開銷的鹃觉。
一般偏向鎖是在有不同線程申請鎖時升級為輕量鎖专酗,這也就意味著假如一個對象先被線程1加鎖解鎖,再被線程2加鎖解鎖盗扇,這過程中沒有鎖沖突祷肯,也一樣會發(fā)生偏向鎖失效,不同的是這回要先退化為無鎖的狀態(tài)疗隶,再加輕量鎖佑笋。
引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令斑鼻,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由于一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖蒋纬,所以偏向鎖的撤銷操作的性能損耗必須小于節(jié)省下來的CAS原子指令的性能消耗)。
偏向鎖獲取過程:
(1) 訪問Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1,鎖標(biāo)志位是否為01——確認(rèn)為可偏向狀態(tài)蜀备。
(2) 如果為可偏向狀態(tài)关摇,則測試線程ID是否指向當(dāng)前線程,如果是碾阁,進(jìn)入步驟(5)输虱,否則進(jìn)入步驟(3)。
(3) 如果線程ID并未指向當(dāng)前線程脂凶,則通過CAS操作競爭鎖宪睹。如果競爭成功,則將Mark Word中線程ID設(shè)置為當(dāng)前線程ID艰猬,然后執(zhí)行(5)横堡;如果競爭失敗,執(zhí)行(4)冠桃。
(4)如果CAS獲取偏向鎖失敗,則表示有競爭道宅。當(dāng)?shù)竭_(dá)全局安全點(safepoint)時獲得偏向鎖的線程被掛起食听,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼污茵。
(5) 執(zhí)行同步代碼樱报。偏向鎖的釋放:
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他線程嘗試競爭偏向鎖時泞当,持有偏向鎖的線程才會釋放鎖迹蛤,線程不會主動去釋放偏向鎖。偏向鎖的撤銷襟士,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行)盗飒,它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài)陋桂,撤銷偏向鎖后恢復(fù)到未鎖定(標(biāo)志位為“01”)或輕量級鎖(標(biāo)志位為“00”)的狀態(tài)逆趣。
輕量級鎖
“輕量級”是相對于使用操作系統(tǒng)互斥量來實現(xiàn)的傳統(tǒng)鎖而言的。但是嗜历,首先需要強(qiáng)調(diào)一點的是宣渗,輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下梨州,減少傳統(tǒng)的重量級鎖使用產(chǎn)生的性能消耗痕囱。在解釋輕量級鎖的執(zhí)行過程之前,先明白一點暴匠,輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的情況鞍恢,如果存在同一時間訪問同一鎖的情況,就會導(dǎo)致輕量級鎖膨脹為重量級鎖。
-
輕量級鎖的加鎖過程
∮行颉(1)在代碼進(jìn)入同步塊的時候抹腿,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為“01”狀態(tài),是否為偏向鎖為“0”)旭寿,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間警绩,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word盅称。這時候線程堆棧與對象頭的狀態(tài)如圖2.1所示肩祥。
(2)拷貝對象頭中的Mark Word復(fù)制到鎖記錄中缩膝。
』旌荨(3)拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針疾层,并將Lock record里的owner指針指向object mark word将饺。如果更新成功,則執(zhí)行步驟(3)痛黎,否則執(zhí)行步驟(4)予弧。
(4)如果這個更新動作成功了湖饱,那么這個線程就擁有了該對象的鎖掖蛤,并且對象Mark Word的鎖標(biāo)志位設(shè)置為“00”,即表示此對象處于輕量級鎖定狀態(tài)井厌,這時候線程堆棧與對象頭的狀態(tài)如圖2.2所示蚓庭。
(5)如果這個更新操作失敗了仅仆,虛擬機(jī)首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀器赞,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行蝇恶。否則說明多個線程競爭鎖拳魁,輕量級鎖就要膨脹為重量級鎖,鎖標(biāo)志的狀態(tài)值變?yōu)椤?0”撮弧,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針潘懊,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。 而當(dāng)前線程便嘗試使用自旋來獲取鎖贿衍,自旋就是為了不讓線程阻塞授舟,而采用循環(huán)去獲取鎖的過程。
- 輕量級鎖的解鎖過程:
∶潮病(1)通過CAS操作嘗試把線程中復(fù)制的Displaced Mark Word對象替換當(dāng)前的Mark Word释树。
(2)如果替換成功,整個同步過程就完成了奢啥。
〗障伞(3)如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹)桩盲,那就要在釋放鎖的同時寂纪,喚醒被掛起的線程。
重量級鎖
TBD
重量級鎖赌结、輕量級鎖和偏向鎖之間轉(zhuǎn)換
volatile 關(guān)鍵字
又是面試中經(jīng)常會被問到的一個Java關(guān)鍵字捞蛋,如果用volatile聲明一個變量為共享的,一個線程修改了某個變量的值柬姚,這個更新的值會立即寫入內(nèi)存中拟杉,從而對其他線程來說是立即可見的。
然而比較坑的結(jié)果量承,volatile只能在很小的范圍內(nèi)保證互斥性搬设,如果對volatile變量本身的操作不是線程安全的,比如++宴合,那么同樣是有問題的焕梅。
volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性
具體的使用場景參照Java 理論與實踐: 正確使用 Volatile 變量卦洽。不過我膚淺的總結(jié)起來就是盡量不要用volatile來實現(xiàn)互斥。斜棚。阀蒂。。
還有個典型的使用volatile的場景實在多線程環(huán)境下lazy load的單例模式弟蚀,參考Java 單例真的寫對了么?
volatile底層實現(xiàn)
雖然volatile不建議使用蚤霞,但是還是有必要探究一下它在底層是如何實現(xiàn)的,因為有助于更好的理解JVM的編譯機(jī)制义钉。
為了優(yōu)化性能昧绣,編譯器和CPU可能對某些指令進(jìn)行重排。大家都知道java代碼最終會被編譯成匯編指令捶闸,而一條java語句可能對應(yīng)多條匯編指令夜畴。為了優(yōu)化性能,CPU和編譯器會對這些指令重排删壮,volatile的變量在進(jìn)行操作只會在尾部添加一個內(nèi)存屏障(Memory Barrier)贪绘,lock addl $0x0,(%rsp)。它可以:a) 確保一些特定操作執(zhí)行的順序央碟; b) 影響一些數(shù)據(jù)的可見性(可能是某些指令執(zhí)行后的結(jié)果)税灌。編譯器和CPU可以在保證輸出結(jié)果一樣的情況下對指令重排序,使性能得到優(yōu)化。插入一個內(nèi)存屏障菱涤,相當(dāng)于告訴CPU和編譯器先于這個命令的必須先執(zhí)行苞也,后于這個命令的必須后執(zhí)行。內(nèi)存屏障另一個作用是強(qiáng)制更新一次不同CPU的緩存粘秆。例如如迟,一個寫屏障會把這個屏障前寫入的數(shù)據(jù)刷新到緩存,這樣任何試圖讀取該數(shù)據(jù)的線程將得到最新值翻擒,而不用考慮到底是被哪個cpu核心或者哪顆CPU執(zhí)行的氓涣。所以一旦你完成寫入,任何訪問這個變量的線程將會得到最新的值陋气。而且在你寫入前劳吠,會保證所有之前發(fā)生的事已經(jīng)發(fā)生,并且任何更新過的數(shù)據(jù)值也是可見的巩趁,因為內(nèi)存屏障會把之前的寫入值都刷新到緩存痒玩。
更多volatile的細(xì)節(jié),請看深入理解Java內(nèi)存模型(四)——volatile
concurrent包
java.util.concurrent在java 1.5以后提供了另一種多線程編程的方式议慰,主要提供了各種鎖蠢古,線程池和基本類型的atomic版本。
大致的讀了一下代碼别凹,以下通過一些源碼探索底層的實現(xiàn)草讶。
ReentrentLock
ReentrentLock和synchronized關(guān)鍵字類似,也是實現(xiàn)一種可重入的鎖炉菲。
ReentrentLock實現(xiàn)了Lock接口堕战,內(nèi)部定義了兩個Sync類,F(xiàn)airSync和NonFairSync拍霜,分別實現(xiàn)公平鎖和非公平鎖嘱丢。 該類又繼承自AbstractQueuedSynchronizer類。類圖如下:
AbstractQueuedSynchronizer實際上在內(nèi)部維護(hù)了一個列表形式的等待隊列祠饺,每個node都記錄了一個線程和等待的狀態(tài)越驻。
關(guān)鍵代碼:
ReentrantLock
public void lock() {
sync.lock();
}
Sync
abstract void lock();
抽象方法,在FairSync和NonFairSync里都有相應(yīng)的實現(xiàn)道偷,先看FairSync
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
關(guān)鍵的地方來了缀旁,tryAcquire里有個compareAndState方法。
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
它會調(diào)用unsafe的compareAndSwapInt试疙,查看一下Unsafe類诵棵,這是一系列基于JNI的API定義類,其中有一些compareAndSwap方法祝旷,縮寫為CAS履澳。這個方法會在CPU級別來支持原子性嘶窄。
C++代碼
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
程序會根據(jù)當(dāng)前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行距贷,就為cmpxchg指令加上lock前綴(lock cmpxchg)柄冲。反之,如果程序是在單處理器上運行忠蝗,就省略lock前綴(單處理器自身會維護(hù)單處理器內(nèi)的順序一致性现横,不需要lock前綴提供的內(nèi)存屏障效果)。
更多CAS的原理阁最,可以參考 JAVA CAS原理深度分析
回到FairSync中戒祠,tryAcquire方法會檢查:
- 如果鎖數(shù)量為0,如果當(dāng)前線程是等待隊列中的頭節(jié)點速种,基于CAS嘗試將state(鎖數(shù)量)從0設(shè)置為1一次姜盈,如果設(shè)置成功,設(shè)置當(dāng)前線程為獨占鎖的線程
- 如果鎖數(shù)量不為0或者當(dāng)前線程不是等待隊列中的頭節(jié)點或者上邊的嘗試又失敗了配阵,查看當(dāng)前線程是不是已經(jīng)是獨占鎖的線程了馏颂,如果是,則將當(dāng)前的鎖數(shù)量+1(可重入)棋傍。如果不是, 則將該線程封裝在一個Node內(nèi)救拉,并加入到等待隊列中去, 等待被其前一個線程節(jié)點喚醒。
如果第一時間沒有獲取到鎖瘫拣,沒關(guān)系亿絮,接著
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
這種死循環(huán)的方式申請鎖有個好聽的名字,叫“自旋”麸拄。
lock看完了再看看unlock壹无,原理類似:
Reentrant
public void unlock() {
sync.release(1);
}
Sync
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
LockSupport.park()、LockSupport.unpark()底層還是調(diào)用Unsafe累的park/unpark方法感帅,作用分別是阻塞線程和解除阻塞線程,且park()和unpark()不會遇到“Thread.suspend ()和 Thread.resume所可能引發(fā)的死鎖”問題地淀。