前言
本文已經(jīng)收錄到我的 Github 個人博客递惋,歡迎大佬們光臨寒舍:
學習導圖
一.為什么要學習內(nèi)存模型與線程汇荐?
之前我們學習了內(nèi)存模型和線程蚊逢,了解了
JMM
和線程韭山,初步探究了JVM
怎么實現(xiàn)并發(fā)功蜓,而本篇文章嗤堰,我們的關注點是JVM
如何實現(xiàn)高效
并發(fā)編程的目的是為了讓程序運行得更快戴质,提高程序的響應速度度宦,雖然我們希望通過多線程執(zhí)行任務讓程序運行得更快,但是同時也會面臨非常多的挑戰(zhàn)告匠,比如像線程安全問題戈抄、線程上下文切換的問題、硬件和軟件資源限制等問題后专,這些都是并發(fā)編程給我們帶來的難題划鸽。
其中線程安全問題是我們最關心的問題之一,我們接下來主要就圍繞著線程安全的問題來展開戚哎。
二.核心知識點歸納
2.1 線程安全
2.1.1 定義
當多個線程訪問一個對象時裸诽,如果不用考慮這些線程在運行時環(huán)境下的調度和交替執(zhí)行,也不需要進行額外的同步型凳,或者在調用方進行任何其他的協(xié)調操作丈冬,調用這個對象的行為都可以獲得正確的結果,那這個對象是線程安全的
要求線程安全的代碼都必須具備一個特征:
代碼本身封裝了所有必要的正確性保障手段(如互斥同步等)甘畅,令調用者無須關心多線程的問題埂蕊,更無須自己采取任何措施來保證多線程的正確調用。
2.1.2 分類
下面將按照線程安全的程度由強至弱分成五類
- 不可變:外部的可見狀態(tài)永遠不會改變疏唾,在多個線程之中永遠是一致的狀態(tài)
一定是線程安全的
如何實現(xiàn):
1.如果共享數(shù)據(jù)是一個基本數(shù)據(jù)類型蓄氧,只要在定義時用
final
關鍵字修飾2.如果共享數(shù)據(jù)是一個對象,最簡單的方法是把對象中帶有狀態(tài)的變量都聲明為
final
(例如String
類的實現(xiàn))
- 絕對線程安全:完全滿足之前給出的線程安全的定義槐脏,即達到『不管運行時環(huán)境如何喉童,調用者都不需要任何額外的同步措施』
- 相對線程安全:能保證對該對象單獨的操作是線程安全的,在調用時無需做額外保障措施准给,但對于一些特定順序的連續(xù)調用泄朴,可能需要在調用端使用額外的同步措施來保證調用的正確性
- 是通常意義上所講的線程安全
- 大部分的線程安全類都屬于這種類型,如
Vector
露氮、HashTable
祖灰、Collections#synchronizedCollection()
包裝的集合等
- 線程兼容:對象本身非線程安全的,但可以通過在調用端正確地使用同步手段來保證對象在并發(fā)環(huán)境中可以安全地使用
- 是通常意義上所講的非線程安全
Java API
中大部分類都是屬于線程兼容的畔规,如ArrayList
和HashMap
等
- 線程對立:無論調用端是否采取了同步措施局扶,都無法在多線程環(huán)境中并發(fā)使用的代碼
例子:
Thread
類的suspend()
和resume()
,一個嘗試中斷線程叁扫,一個嘗試恢復線程三妈,在并發(fā)條件下,有可能會造成死鎖
2.1.3 實現(xiàn)
可分成兩大手段:
- 通過代碼編寫實現(xiàn)線程安全
- 通過虛擬機本身實現(xiàn)同步與鎖
本篇重點在虛擬機本身
1.互斥同步
- 含義:
同步:在多個線程并發(fā)訪問共享數(shù)據(jù)時莫绣,保證共享數(shù)據(jù)在同一個時刻只被一個線程使用
互斥:是實現(xiàn)同步的一種手段畴蒲,臨界區(qū)、互斥量和信號量都是主要的互斥實現(xiàn)方式
兩者關系:互斥是因对室,同步是果模燥;互斥是方法咖祭,同步是目的
屬于悲觀并發(fā)策略(悲觀鎖),即認為只要不做正確的同步措施就肯定會出現(xiàn)問題蔫骂,因此無論共享數(shù)據(jù)是否真的會出現(xiàn)競爭么翰,都要加鎖
最大的問題是進行線程阻塞和喚醒所帶來的性能問題,也稱為阻塞同步
-
使用方式:
A.使用
synchronized
關鍵字:
原理:編譯后會在同步塊的前后分別形成
monitorenter
和monitorexit
這兩個字節(jié)碼指令辽旋,并通過一個reference
類型的參數(shù)來指明要鎖定和解鎖的對象注意:
? 1.若明確指定了對象參數(shù)浩嫌,則取該對象的
reference
? 2.否則,會根據(jù)
synchronized
修飾的是實例方法還是類方法去取對應的對象實例或Class
對象來作為鎖對象過程:執(zhí)行
monitorenter
指令時先要嘗試獲取對象的鎖补胚。若該對象沒被鎖定或者已被當前線程獲取码耐,那么鎖計數(shù)器+ 1
;而在執(zhí)行monitorexit
指令時糖儡,鎖計數(shù)器- 1
伐坏;當鎖計數(shù)器 =0
時怔匣,鎖就被釋放握联;若獲取對象鎖失敗,那當前線程會一直被阻塞等待每瞒,直到對象鎖被另外一個線程釋放為止特別注意:
1.
synchronized
同步塊對同一條線程來說是可重入的金闽,不會出現(xiàn)自我鎖死的問題2.同步塊在已進入的線程執(zhí)行完之前,會阻塞后面其他線程的進入
? B.使用重入鎖 ReentrantLock
:
? 之前在 進階之路 | 奇妙的 Thread 之旅中也提到過重入鎖的使用剿骨,相信看過的讀者還有一些印象
與
synchronized
的相同:用法與synchronized
很相似代芜,且都可重入與
synchronized
的不同:1.等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待浓利,改為處理其他事情
2.公平鎖:多個線程在等待同一個鎖時挤庇,必須按照申請鎖的時間順序來依次獲得鎖。而
synchronized
是非公平的贷掖,即在鎖被釋放時嫡秕,任何一個等待鎖的線程都有機會獲得鎖。ReentrantLock
默認情況下也是非公平的苹威,但可以通過帶布爾值的構造函數(shù)改用公平鎖3.鎖綁定多個條件:一個
ReentrantLock
對象可以通過多次調用newCondition()
同時綁定多個Condition
對象昆咽。而在synchronized
中,鎖對象的wait()
和notify()
或notifyAl()
只能實現(xiàn)一個隱含的條件牙甫,若要和多于一個的條件關聯(lián)不得不額外地添加一個鎖
- 選擇:在
synchronized
能實現(xiàn)需求的情況下掷酗,優(yōu)先考慮使用它來進行同步。理由如下:
synchronized
是Java
語法層面的同步窟哺,足夠清晰簡單Lock
必須由程序員確保在finally
塊中釋放鎖泻轰,而synchronized
可以由JVM
確保鎖的自動釋放
2.非阻塞同步
- 定義:基于沖突檢測的樂觀并發(fā)策略(樂觀鎖),即先進行操作且轨,若無其他線程爭用共享數(shù)據(jù)浮声,操作成功亩鬼;反之產(chǎn)生了沖突再去采取其他的補償措施
- 為了保證操作和沖突檢測這兩步具備原子性,需要用到硬件指令集阿蝶,比如:
- 測試并設置
- 獲取并增加
- 交換
- 比較并交換(
CAS
)- 加載鏈接 / 條件存儲
3.無同步方案
- 定義:不用同步的方式保證線程安全雳锋,因為有些代碼天生就是線程安全的。
- 例子:
A.可重入代碼/ 純代碼
- 含義:可在代碼執(zhí)行的任何時刻中斷它去執(zhí)行另外一段代碼羡洁,當控制權返回后原來的程序并不會出現(xiàn)任何錯誤
- 共同特征:不依賴存儲在堆上的數(shù)據(jù)和公用的系統(tǒng)資源玷过、用到的狀態(tài)量都由參數(shù)中傳入、不調用非可重入的方法
- 判定依據(jù):如果一個方法筑煮,它的返回結果是可預測的辛蚊,只要輸入相同的數(shù)據(jù)就都能返回相同的結果,就滿足可重入性
- 注意:滿足可重入性的代碼一定是線程安全的真仲,反之袋马,滿足線程安全的代碼不一定是可重入的
B.線程本地存儲
- 含義:把共享數(shù)據(jù)的可見范圍限制在同一個線程之內(nèi),無須同步就能保證線程之間不出現(xiàn)數(shù)據(jù)爭用的問題
- 想詳細了解
ThreadLocal
的讀者秸应,可以看下筆者之前寫的一篇文章:進階之路 | 奇妙的 Handler 之旅
2.2 鎖優(yōu)化
解決并發(fā)的正確性之后虑凛,為了能在線程之間更『高效』地共享數(shù)據(jù)、解決競爭問題软啼、提高程序的執(zhí)行效率桑谍,下面介紹五種鎖優(yōu)化技術
2.2.1 適應性自旋
- 背景:互斥同步在實現(xiàn)阻塞和喚醒時需要掛起線程和恢復線程的操作,都需要轉入內(nèi)核態(tài)中完成祸挪,很影響系統(tǒng)的并發(fā)性能锣披;同時,在許多應用上共享數(shù)據(jù)的鎖定狀態(tài)只是暫時贿条,沒必要去掛起和恢復線程
- 自旋鎖:當物理機器有多個處理器使得多個線程同時并行執(zhí)行時雹仿,先讓后請求鎖的線程等待,但不放棄處理器的執(zhí)行時間整以,看看持有鎖的線程是否很快就會釋放鎖胧辽,這時只需讓線程執(zhí)行一個忙循環(huán),即自旋
注意:自旋等待不能代替阻塞悄蕾,它雖然能避免線程切換的開銷票顾,但會占用處理器時間,因此自旋等待的時間必須要有一定的限度帆调,如果自旋超過了限定的次數(shù)(默認
10
次)仍未成功獲鎖奠骄,就需要掛線程了
- 自適應自旋鎖:自旋的時間不再固定,而是由該鎖上的上次自旋時間及鎖的擁有者的狀態(tài)共同決定番刊。具體表現(xiàn)是:
- 如果對于某個鎖含鳞,自旋等待剛剛成功獲得,且持有鎖的線程正在運行中芹务,那么虛擬機很可能允許自旋等待的時間更久點
- 如果對于某個鎖蝉绷,自旋很少成功獲得過鸭廷,那么很可能以后將省略自旋等待這個鎖,避免浪費處理器資源
2.2.2 鎖消除
- 定義:指虛擬機即時編譯器在運行時熔吗,對一些代碼上要求同步辆床,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除
- 判定依據(jù):如果一段代碼中堆上的所有數(shù)據(jù)都不會逃逸出去被其他線程訪問到,可把它們當做棧上數(shù)據(jù)對待桅狠,即線程私有的讼载,無須同步加鎖
2.2.3 鎖粗化
- 一般情況下,會將同步塊的作用范圍限制到只在共享數(shù)據(jù)的實際作用域中才進行同步中跌,使得需要同步的操作數(shù)量盡可能變小咨堤,保證就算存在鎖競爭,等待鎖的線程也能盡快拿到鎖
- 但如果反復操作對同一個對象進行加鎖和解鎖漩符,即使沒有線程競爭一喘,頻繁地進行互斥同步操作也會導致不必要的性能損耗,此時嗜暴,虛擬機將會把加鎖同步的范圍粗化到整個操作序列的外部凸克,這樣只需加一次鎖
2.2.4 輕量級鎖
- 目的:在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗灼伤,注意不是用來代替重量級鎖的
首先先理解
HotSpot
虛擬機的對象頭的內(nèi)存布局:分為兩部分
- 第一部分用于存儲對象自身的運行時數(shù)據(jù)触徐,這部分被稱為
Mark Word
咪鲜,是實現(xiàn)輕量級鎖和偏向鎖的關鍵狐赡。如哈希碼、GC
分代年齡等- 另外一部分用于存儲指向方法區(qū)對象類型數(shù)據(jù)的指針疟丙,如果是數(shù)組對象還會有一個額外的部分用于存儲數(shù)組長度
-
加鎖過程:
1.代碼進入同步塊時颖侄,如果同步對象未被鎖定(鎖標志位為
01
),虛擬機會在當前線程的棧幀中建立一個名為Lock Record
的空間享郊,用于存儲鎖對象Mark Word
的拷貝览祖。如下圖
2.之后虛擬機會嘗試用 CAS
操作將對象的 Mark Word
更新為指向 Lock Record
的指針。若更新動作成功炊琉,那么當前線程就擁有了該對象的鎖展蒂,且對象 Mark Word
的鎖標志位變?yōu)?00
,即處于輕量級鎖定狀態(tài)苔咪;反之锰悼,虛擬機會先檢查對象的 Mark Word
是否指向當前線程的棧幀,若是团赏,則當前線程已有該對象的鎖箕般,可直接進入同步塊繼續(xù)執(zhí)行,否則說明改對象已被其他線程搶占舔清。如下圖:
另外丝里,如果有兩條以上的線程爭用同一個鎖曲初,那輕量級鎖就不再有效,要膨脹為重量級鎖杯聚,鎖標志位變?yōu)?
10
臼婆,Mark Word
中存儲的就是指向重量級鎖的指針,后面等待鎖的線程也要進入阻塞狀態(tài)
-
解鎖過程:若對象的
Mark Word
仍指向著線程的Lock Record
幌绍,就用CAS
操作把對象當前的Mark Word
和線程中復制的Displaced Mark Word
替換回來目锭。若替換成功,那么就完成了整個同步過程纷捞;反之痢虹,說明有其他線程嘗試獲取該鎖,那么就要在釋放鎖的同時喚醒被掛起的線程 優(yōu)點:因為對于絕大部分的鎖主儡,在整個同步周期內(nèi)都是不存在競爭的奖唯,所以輕量級鎖通過使用
CAS
操作消除同步使用的互斥量自旋鎖和輕量級鎖的關系:
- 自旋鎖是為了減少線程掛起次數(shù)
- 輕量級鎖是在加鎖的時候,如何使用一種更高效的方式來加鎖
Q:處于輕量級鎖狀態(tài)時糜值,會不會使用自旋鎖這個競爭機制
A:線程首先會通過
CAS
獲取鎖丰捷,失敗后通過自旋鎖來嘗試獲取鎖,再失敗鎖就膨脹為重量級鎖寂汇。所以輕量級鎖狀態(tài)下可能會有自旋鎖的參與(CAS
將對象頭的標記指向鎖記錄指針失敗的時候)
2.2.5 偏向鎖
- 目的:消除數(shù)據(jù)在無競爭情況下的同步原語病往,進一步提高程序的運行性能
如果說輕量級鎖是在無競爭的情況下使用
CAS
去消除同步使用的互斥量那偏向鎖就是在無競爭情況下把整個同步都消除掉
含義:偏向鎖會偏向于第一個獲得它的線程,如果在后面的執(zhí)行中該鎖沒有被其他的線程獲取骄瓣,則持有偏向鎖的線程將永遠不需要再進行同步
加鎖過程:啟用偏向鎖的鎖對象在第一次被線程獲取時停巷,
Mark Word
的鎖標志位會被設置為01
,即偏向模式榕栏,同時使用CAS
操作把獲取到這個鎖的線程ID
記錄在對象的Mark Word
中畔勤。若操作成功,持有偏向鎖的線程以后每次進入這個鎖相關的同步塊時都可不再進行任何同步操作-
解鎖過程:當有另外的線程去嘗試獲取這個鎖時扒磁,根據(jù)鎖對象目前是否處于被鎖定的狀態(tài)庆揪,撤銷偏向后恢復到未鎖定
01
或輕量級鎖定00
的狀態(tài),后續(xù)的同步操作就如輕量級鎖執(zhí)行過程妨托。如下圖: 優(yōu)點:可提高帶有同步但無競爭的程序性能缸榛,但若程序中大多數(shù)鎖總被多個線程訪問,此模式就沒必要了
三.碎碎念
能夠寫出高性能兰伤、高伸縮性的并發(fā)程序是一門藝術内颗,而了解并發(fā)在底層是如何實現(xiàn)的,則是掌握這門藝術的前提医清,也是成長為高級程序員的必備知識弯院!
加油吧历葛!騷年酬屉!以夢為馬,不負韶華筒捺!
如果文章對您有一點幫助的話,希望您能點一下贊纸厉,您的點贊系吭,是我前進的動力
本文參考鏈接: