1. volatile的應(yīng)用
volatile是輕量級synchronized, 保證了共享變量的可見性, 可見性的意思當(dāng)一個線程修改一個共享變量時, 其他線程能讀取到這個修改的值, volatile變量的使用比synchronized的成本更低, volatile關(guān)鍵字不會引起線程上下文切換和調(diào)度
1.1 volatile的定義與實現(xiàn)原理
術(shù)語 | 英文單詞 | 術(shù)語描述 |
---|---|---|
內(nèi)存屏障 | memory barries | 一組CPU指令,用于實現(xiàn)對內(nèi)存操作的順序限制 |
CPU緩存 | cache | 緩存了內(nèi)存地址,以及對應(yīng)的數(shù)據(jù) |
緩沖行 | cache line | CPU緩存可以分配的最小存儲單位,CPU修改一個緩存會影響整個緩存行 |
原子操作 | atomic operations | 不可中斷的一個或一系列操作 |
緩存行填充 | cache line fill | CPU從內(nèi)存中讀取的數(shù)據(jù)是可緩存的,CPU將數(shù)據(jù)緩存到L1,L2,L3的緩存行中 |
緩存命中 | cache hit | CPU直接從緩存中讀取數(shù)據(jù) |
寫命中 | write hit | CPU將操作數(shù)再次寫回緩存行 |
寫缺失 | write misses the cache | CPU寫入的數(shù)據(jù),不在CPU緩存中 |
volatile如何保證內(nèi)存可見
為了提高處理速度, CPU不直接與內(nèi)存通信, 而是將內(nèi)存的數(shù)據(jù)讀到CPU緩存中再進行操作, 但操作完成之后, 不知道何時寫入到內(nèi)存
修改了volatile關(guān)鍵字修飾共享變量之后
- JVM會下發(fā)一個Lock指令到CPU, 將CPU緩存寫回到內(nèi)存
- LOCK信號一般不鎖總線, 而是緩存鎖, 總線鎖開銷比較大
- 鎖定內(nèi)存區(qū)域的緩存, 并將其寫回到內(nèi)存, 使用了緩存一致性機制來確保修改的原子性
- 緩存一致性會阻止同時修改兩個以上CPU緩存的內(nèi)存區(qū)域數(shù)據(jù)
- 一個CPU的緩存寫回到內(nèi)存之后, 會導(dǎo)致其他處理器對于volatile共享變量的緩存無效
- 使用MESI控制協(xié)議維護內(nèi)部緩存和其他CPU緩存一致性
- MESI: 每個緩存行使用4種狀態(tài)進行標記
- M: 被修改(Modified), 與內(nèi)存不一致, 緩存就需要在之后的某個時間點, 將數(shù)據(jù)寫回到內(nèi)存
- E: 獨享(Exclusive), 緩存行被一個CPU緩存, 并且數(shù)據(jù)和內(nèi)存一致, 之后如果被其他CPU訪問, 那么就會變成共享的
- S: 共享(Shared), 緩存行被多個CPU共享, 當(dāng)有一個CPU修改了緩存行, 緩存行就會被作廢, 變成無效狀態(tài)
- I: 無效(Invalid), 緩存無效, 可能是其他CPU修改了緩存行
1.2 volatile的使用優(yōu)化
追加字節(jié)優(yōu)化性能, JDK1.8使用@Contended注解填充緩存行
- CPU使用緩存行有固定長度的, 例如64位, 256位
- 如果緩存對象沒有達到最小單位, 會將多個緩存對象緩存到一個緩存行
- 如果緩存對象不會被頻繁修改, 那么沒有必要直接字節(jié)
2. synchronized的實現(xiàn)原理與應(yīng)用
JDK1.6對synchronized關(guān)鍵字進行了各種優(yōu)化, 為了減少獲取和釋放鎖帶來的性能消耗, 引入了偏向鎖和輕量級鎖, 以及鎖的存儲結(jié)構(gòu)和升級過程
Java中的每一個對象都可以作為鎖
- 普通同步方法, 鎖是當(dāng)前實例對象(this)
- 靜態(tài)同步方法, 鎖是當(dāng)前類的Class對象
- 同步方法塊, 鎖是synchronized括號里設(shè)置的對象
2.1 java對象頭
synchronized用的對象鎖是放在Java對象頭里面的
- 如果對象是數(shù)組類型, 那么虛擬機使用3字寬(Word)存儲對象頭
- 如果對象是非數(shù)組類型, 那么使用2字寬存儲對象頭
- 32位虛擬機中, 1字寬等于4字節(jié)(Byte), 即32bit
- 64位虛擬機中, 1字寬等于8字節(jié)(Byte), 即64bit
長度 | 內(nèi)容 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashcode或者鎖信息 |
32/64bit | Class Metadata Address | 存儲對象類型的指針 |
32/64bit | Array length | 如果當(dāng)前對象是數(shù)組, 那么存儲數(shù)組長度 |
Mark Word與鎖之間的對應(yīng)關(guān)系
Java1.6為了減少獲得鎖和釋放鎖帶來的消耗, 引入偏向鎖和輕量級鎖, 鎖一共有4種狀態(tài), 級別從低到高為無鎖狀態(tài), 偏向鎖狀態(tài), 輕量級鎖狀態(tài)和重量級鎖狀態(tài)
鎖可以升級但不能降級, 也就是偏向鎖升級為輕量級鎖之后, 不能降級為偏向鎖
2.2 鎖升級與對比
2.2.1 偏向鎖
大多數(shù)情況下, 鎖不存在多線程競爭, 并且總是由同一個線程多次獲取, 為了讓線程獲得鎖獲得鎖的代價更小, 引入了偏向鎖
偏向鎖是最樂觀的一種情況: 只有一個線程請求同一把鎖
(1).偏向鎖的獲取
當(dāng)一個線程訪問同步塊并獲取鎖的時候, 會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID, 之后線程訪問同步代碼塊的時候, 不需要進行使用CAS自旋鎖, 只要驗證一下對象頭的Mark Word里是否存放指向當(dāng)前線程的偏向鎖
如果存儲了指向當(dāng)前線程的偏向鎖, 表示線程已經(jīng)獲取鎖, 可以直接訪問同步代碼塊
-
如果沒有存儲指向當(dāng)前線程的偏向鎖, 則判斷Mark Word中偏向鎖標識是否是1, 表示當(dāng)前是偏向鎖
- 如果是1的話, 嘗試使用CAS設(shè)置Mark Word偏向鎖線程ID, 指向當(dāng)前線程
- 如果不是1的話, 則嘗試使用CAS競爭鎖
(2).偏向鎖的撤銷
偏向鎖要等到競爭出現(xiàn)才會釋放鎖, 當(dāng)其他線程嘗試競爭偏向鎖時, 持有偏向鎖的線程才會在全局安全點(鎖安全點,沒有正在執(zhí)行的字節(jié)碼)釋放鎖
- 先暫停擁有偏向鎖的線程
- 檢查持有偏向鎖的線程是否活著
- 不處于活動狀態(tài)的話, 則將對象頭設(shè)置為無鎖狀態(tài)
- 線程還活著的話, 遍歷偏向?qū)ο蟮逆i記錄, 棧中鎖記錄和對象頭的Mark Word可能執(zhí)行三種操作
- 偏向于其他線程
- 恢復(fù)到無鎖狀態(tài)
- 線程仍然在執(zhí)行同步代碼塊, 會升級為輕量級鎖
- 最后喚醒暫停的線程
輕量級鎖
通過自旋CAS來競爭鎖, 競爭的線程不會阻塞, 如果一直競爭不到鎖, 會消耗CPU
(1).加鎖
線程在執(zhí)行同步代碼塊之前, JVM會在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間, 并將對象頭的Mark Word復(fù)制到鎖記錄中, 官方稱Displaced Mark Word, 然后線程使用CAS將對象頭的Mark Word替換為指向鎖記錄的指針
- 如果成功, 將Mark Word的鎖標記位設(shè)置為00,表示鎖對象處于輕量級鎖狀態(tài), 線程獲取到鎖
- 如果失敗, 表示其他線程競爭鎖, 當(dāng)前線程嘗試使用CAS自旋獲取鎖, 自旋達到一定次數(shù)還沒有獲取到鎖, 會升級為重量級鎖
(2).解鎖
使用CAS操作將Displaced Mark Word替換回對象頭
- 如何設(shè)置成功, 表示沒有發(fā)生競爭
- 如果設(shè)置失敗, 表示當(dāng)前所鎖存在競爭, 就會膨脹為重量級鎖
重量級鎖
使用操作系統(tǒng)的互斥鎖(Mutex Lock)實現(xiàn), 當(dāng)進入重量級鎖之后, 就不會再降級為輕量級鎖, 當(dāng)其他線程占用鎖之后, 當(dāng)前線程會進入堵塞狀態(tài)
每個對象都擁有自己的監(jiān)視器, 線程必須要先獲取到對象監(jiān)視器才能進入同步塊或者同步方法, 沒有獲取到監(jiān)視器的線程將會被堵塞在同步代碼的入口處, 進入blocked狀態(tài)
對象監(jiān)視器依賴操作系統(tǒng)底層的mutex lock(互斥鎖)實現(xiàn)
3. 原子操作的實現(xiàn)原理
原子操作是不可被中斷的一個或者一系列操作
3.1 術(shù)語定義
術(shù)語名稱 | 解釋 | |
---|---|---|
緩存行 | Cache line | 緩存的最小操作單位 |
比較并交換 | Compare and Swap | CAS操作需要輸入兩個值, 一個舊值和一個新值, 操作之前比較舊值有沒有發(fā)生變化, 如果沒有發(fā)生變化, 才會交換成新值 |
CPU流水線 | CPU pipeline | 在CPU中, 由5到6個不同功能的電路單元組成一條指令處理流水線, 然后將一條X86指令分成5到6步后再由這些單元分別執(zhí)行, 這樣就能實現(xiàn)在一個CPU時鐘周期完成一條指令, 提高CPU運算速度 |
內(nèi)存順序沖突 | Memory order violation | 內(nèi)存順序沖突一般是由假共享引起的, 假共享是指多個CPU同時修改同一個緩存行的不同部分, 導(dǎo)致其中一個CPU操作無效, 當(dāng)出現(xiàn)內(nèi)存順序沖突時, CPU必須清空流水線 |
3.2 處理器如何實現(xiàn)原子操作
如果過個處理器同時對共享變量進行改寫操作, 就會導(dǎo)致共享變量的值和期望的值不一致, 因為多個處理器同時從各自的緩存中讀取變量, 分別操作, 然后分別寫入系統(tǒng)內(nèi)存中
處理器提供了總線鎖定和緩存鎖定兩種機制來保證復(fù)雜的內(nèi)存操作的原子性
3.2.1 總線鎖
CPU提供一個LOCK#信號, 當(dāng)一個處理器在總線上輸出此信號時, 其他處理器的請求將會被阻塞, 該處理器可以獨占共享內(nèi)存
總線鎖把CPU和內(nèi)存的通信鎖定住了, 鎖定期間, 其他處理器不能操作內(nèi)存的數(shù)據(jù), 所以總線鎖開銷比較大
3.2.2 緩存鎖
只需要保證某個內(nèi)存地址的操作是原子性即可, 頻繁使用的內(nèi)存會緩存在CPU高速緩存中, 那么原子操作就可以直接在CPU內(nèi)部緩存中進行, 使用緩存一致性來保證操作的原子性, 一般使用MESI控制協(xié)議, 保證緩存的一致性
有兩種情況CPU不使用緩存鎖定, 而使用總線鎖定:
- 操作的數(shù)據(jù)不能被緩存在CPU內(nèi)部或者操作的數(shù)據(jù)跨多個緩存行
- 有些CPU不支持緩存鎖定
3.3 java如何實現(xiàn)原子操作
java通過鎖和CAS方式來實現(xiàn)原子操作
3.3.1 CAS(Compare and Swap)
對一個內(nèi)存地址進行操作, CAS操作需要輸入兩個值, 一個舊值和一個新值, 操作之前比較舊值有沒有發(fā)生變化, 如果沒有發(fā)生變化, 才會交換成新值
Java中的Atomic原子類, AQS的底層實現(xiàn)都使用了CAS實現(xiàn)
CAS實現(xiàn)原子操作的三大問題:
- ABA問題
- CAS需要在操作值的時候, 檢查值有沒有發(fā)生變化, 如果沒有發(fā)生變化才更新, 但如果一個值原來是A, 變成了B, 又變成了A, 那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化
- 解決思路是使用版本號, 在對象前面追加版本號, 每次版本號加1, JDK中使用AtomicStampedReference類解決ABA問題
public class AtomicStampedReference<V> { /** * 舊值和舊版本號都相同, 才會把對象更新為新值和新版本號 * * @param expectedReference 舊值 * @param newReference 新值 * @param expectedStamp 舊版本號 * @param newStamp 新版本號 * @return {@code true} 如果成功返回true */ public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } }
- 循環(huán)時間長開銷大, 自旋CAS如果長時間不成功, 那么會給CPU帶來非常大的開銷
- 只能保證一個共享變量的原子操作
- JDK提供了AtomicReference類保證引用對象之間的原子性, 可以把多個變量放在一個對象里進行CAS操作
3.3.2 鎖
鎖機制保證了只有獲得鎖的線程才能操作鎖定的內(nèi)存區(qū)域, JVM內(nèi)存實現(xiàn)了多種鎖機制, 有偏向鎖, 輕量級鎖和互斥鎖(重量解鎖)
除了偏向鎖, JVM實現(xiàn)鎖的方式都使用了CAS,
- 當(dāng)有一個線程想進入同步塊的時候使用CAS的方式獲取鎖
- 當(dāng)線程想退出同步塊的時候使用CAS方式釋放鎖