并發(fā)編程之Java內(nèi)存模型鎖的內(nèi)存語義
簡介:
鎖的作用是讓臨界區(qū)互斥執(zhí)行。本文闡述所得另一個重要知識點——鎖的內(nèi)存語義。
1、鎖的釋放-獲取建立的happens-before關(guān)系
鎖是Java并發(fā)編程中最重要的同步機制。鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發(fā)送消息。
鎖釋放-獲取的示例代碼:
package com.lizba.p1; /** * <p> * 鎖示例代碼 * </p> * * @Author: Liziba * @Date: 2021/6/10 21:43 */ public class MonitorExample { int a = 0; public synchronized void writer() { // 1; a++; // 2; } // 3; public synchronized void reader() { // 4; int i = a; // 5; System.out.println(i); } // 6; }
假設(shè)線程A執(zhí)行writer()
方法,隨后線程B執(zhí)行reader()
方法。根據(jù)happens-before
規(guī)范,這個過程包含的happens-before
關(guān)系可以分為3類。
- 根據(jù)程序次序規(guī)則:1 happens-before 2,2 happens-before 3, 4 happens-before 5,5 happens-before 6
- 根據(jù)監(jiān)視器鎖規(guī)則:3 happens-before 4
- 根據(jù)happens-before的傳遞性,2 happens-before 5
上述happens-before關(guān)系的圖形化表現(xiàn)形式如圖:
總結(jié):
線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個鎖之后,將立即變得對B線程可見。
2、鎖釋放和獲取的內(nèi)存語義
當(dāng)線程釋放鎖時,JMM會把該線程對應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。以上述MonitorExample程序為例,A線程釋放鎖后共享數(shù)據(jù)的狀態(tài)
共享數(shù)據(jù)的狀態(tài)示意圖如下所示:
當(dāng)線程獲取鎖時,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效。從而使得被監(jiān)視器鎖保護的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。
鎖獲取的狀態(tài)示意圖:
對比鎖釋放-獲取鎖的內(nèi)存語義與volatile
寫-讀的內(nèi)存語義可以看出:鎖釋放與volatile
寫有相同的內(nèi)存語義;鎖獲取與volatile讀有相同的內(nèi)存語義。
總結(jié):
- 線程A釋放鎖,實質(zhì)上是線程A向接下來要獲取這個鎖的某個線程發(fā)出了(線程A對共享變量所做修改的)消息。
- 線程B獲取鎖,實質(zhì)上是線程B接受了之前某個線程發(fā)出的(在釋放這個鎖對共享變量鎖做的修改的)消息。
- 線程A是否鎖,隨后線程B獲取這個鎖,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
3、鎖內(nèi)存的語義實現(xiàn)
分析ReentrantLock
的源代碼,來分析鎖內(nèi)存語義的具體實現(xiàn)機制。
示例代碼:
package com.lizba.p1; import java.util.concurrent.locks.ReentrantLock; /** * <p> * ReentrantLock示例代碼 * </p> * * @Author: Liziba * @Date: 2021/6/10 22:17 */ public class ReentrantLockExample { int a = 0; ReentrantLock lock = new ReentrantLock(); public void writer() { lock.lock(); // 獲取鎖 try { a++; } finally { lock.unlock(); // 釋放鎖 } } public void reader() { lock.lock(); // 獲取鎖 try { int i = a; System.out.println(i); } finally { lock.unlock(); // 釋放鎖 } } }
在ReentrantLock
中,調(diào)用lock()
方法獲取鎖;調(diào)用unlock()
方法釋放鎖。
ReentrantLock
的實現(xiàn)依賴于Java同步器框架AbstractQueuedSynchronized(AQS)
。AQS使用一個整型的volatile變量(state)來維護同步狀態(tài),這個volatile
變量是ReentrantLock
內(nèi)存語義實現(xiàn)的關(guān)鍵。
ReetrantLock的類圖:
ReentrantLock
分為公平鎖和非公平鎖,首先分析公平鎖。
使用公平鎖時,加鎖方法lock()的調(diào)用軌跡如下:
ReentrantLock: lock(
FairSync: lock()
AbstractQueuedSynchronizer: acquire(int arg)
ReentrantLock: tryAcquire(int acquires)
第4步開始真的加鎖,下面是該方法的源代碼:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 獲取鎖開始,首先讀取volatile變量state 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; }
從上面的代碼中可以看出,加鎖方法首先讀取volatile
變量state
。
在使用公平鎖時,解鎖方法unlock()調(diào)用軌跡如下:
ReentrantLock: unlock()
AbstractQueuedSynchronizer: release(int arg)
Sync: tryRelease(int release)
第3步開始真的釋放鎖,下面是該方法的源代碼:
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); } // 釋放鎖的最后,寫volatile變量state setState(c); return free; }
從上面的代碼中可以看出,釋放鎖的最后寫volatile
變量state
。
總結(jié)公平鎖:
根據(jù)volatile的happens-before規(guī)則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取到同一個volatile變量后將立即變得對獲取鎖的線程可見。
現(xiàn)在分析非公平鎖:
注意:非公平鎖的釋放和公平鎖的釋放完全一致,都是上面的源代碼。所以下面只分析非公平鎖的獲取過程。
使用非公平鎖,加鎖方法lock()的調(diào)用軌跡如下:
ReentrantLock: lock()
NonfairSync: lock()
AbstractQueuedSynchronizer: compareAndSetState(int expect, int update)
第3步開始真的加鎖,下面是該方法的源代碼:
// 方法1 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 此方法中開始加鎖 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } // 方法2 protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this // 該方法是native方法,在JVM中實現(xiàn) return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
該方法以原子操作的方式更新state
變量,也就是compareAndSet()
(CAS)操作。JDK文檔對該方法說明如下:如果當(dāng)前狀態(tài)值等于預(yù)期值,則以原子方式同步狀態(tài)設(shè)置為給定更新的值。此操作具有volatile
讀和寫的內(nèi)存語義。
接下來分別從編譯器和處理器的角度來分析,CAS如何同時具有volatile
讀和volatile
寫的內(nèi)存語義。
編譯器的角度:
前文已經(jīng)講過,編譯器不會對volatile讀與volatile讀后面的任意內(nèi)存操作重排序;編譯器不會對volatile寫和volatile寫后前面的任意內(nèi)存操作重排序。組合這兩個條件,意味著同時實現(xiàn)volatile讀和volatile寫的內(nèi)存語義,編譯器不能對CAS與CAS前面和后面任意內(nèi)存操作重排序。
處理器的角度:
(本人不太懂C++)這一塊總結(jié)需要看JVM源碼,可能會總結(jié)錯誤,如需要深入理解這一塊請查看《Java并發(fā)編程藝術(shù)》53頁。
sun.misc.Unsafe中的compareAndSwapInt源碼如下:(不懂Unsafe請看往期文章)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
這是一個本地方法。這個本地方法會在openJDK
中調(diào)用C++代碼,假設(shè)當(dāng)前是X86處理器,程序會根據(jù)當(dāng)前處理器的類型來決定是非cmpxchg
指令添加lock前綴。
- 程序運行在多處理器上,就為
cmpxchg
指令加上lock前綴(Lock Cmpxchg
) - 程序運行在單處理器上,就省略lock前綴(單處理器自身會維護單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果)
intel手冊對lock前綴的說明:
- 對內(nèi)存的讀-改-寫操作原子執(zhí)行。(總線鎖定/緩存鎖定)
- 禁止該指令,與之前的讀和寫指令重排序
- 把寫緩沖區(qū)的所有數(shù)據(jù)刷新到內(nèi)存中
上面的2、3兩點所具有的內(nèi)存屏障的效果,足以同時實現(xiàn)volatile
讀和volatile
寫的內(nèi)存語義。所以JDK文檔說CAS 具有volatile讀和volatile寫的內(nèi)存語義對于處理器也是符合的。
公平鎖和非公平鎖的總結(jié):
- 公平鎖和非公平鎖的釋放,最后都需要寫一個
volatile
變量state
- 公平鎖獲取時,首先會去讀
volatile
變量 - 非公平鎖獲取鎖時,首先會用
CAS
更新volatile
變量,這個操作同時具有volatile
讀和volatile
寫的內(nèi)存語義
釋放鎖-獲取鎖的內(nèi)存語義的實現(xiàn)方式總結(jié) :
- 利用
volatile
變量的寫-讀所具有的內(nèi)存語義 - 利用CAS所附帶的
volatile
讀和volatile
寫的內(nèi)存語義
4、concurrent包的實現(xiàn)
由于Java的CAS同時具有volatile
讀和volatile
寫的內(nèi)存語義,因此Java線程之間的通信方式有以下4種方式
- A線程寫
volatile
變量,隨后B線程讀這個volatile
變量 - A線程寫
volatile
變量,隨后B線程用CAS更新這個volatile
變量 - A線程利用CAS更新一個volatile變量,隨后B線程用CAS更新這個
volatile
變量 - A線程利用CAS更新一個volatile變量,隨后B線程讀這個
volatile
變量
Java的CAS會使用現(xiàn)代處理器上提供的高效機器級別的原子指令,這些原子指令以原子方式對內(nèi)存執(zhí)行讀-改-寫操作,這是在多處理器實現(xiàn)同步的關(guān)鍵。同時volatile
變量的讀/寫和CAS可以實現(xiàn)線程之間的通信。這些特性就是Java整個concurrent
包的基石。
concurrent包的通用化實現(xiàn)模式:
- 聲明共享變量
volatile
- 使用CAS的原子條件更新來實現(xiàn)線程之間的同步
- 配合
volatile
的讀/寫和CAS具有的volatile
讀和寫的內(nèi)存語義來實現(xiàn)線程之間的通信。
AQS
(java.util.concurrent.locks.AbstractQueuedSynchronizer)、非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中基礎(chǔ)類都是使用這個模式來實現(xiàn)的,而concurrent
包中的高層類又是依賴于這些基礎(chǔ)類。
圖示concurrent包的實現(xiàn)示意圖:
到此這篇關(guān)于并發(fā)編程之Java內(nèi)存模型鎖的內(nèi)存語義的文章就介紹到這了,更多相關(guān)Java內(nèi)存模型鎖的內(nèi)存語義內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
@TransactionalEventListener的使用和實現(xiàn)原理分析
這篇文章主要介紹了@TransactionalEventListener的使用和實現(xiàn)原理分析,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12Spring的@CrossOrigin注解使用與CrossFilter對象自定義詳解
這篇文章主要介紹了Spring的@CrossOrigin注解使用與CrossFilter對象自定義詳解,跨域,指的是瀏覽器不能執(zhí)行其他網(wǎng)站的腳本,它是由瀏覽器的同源策略造成的,是瀏覽器施加的安全限制,所謂同源是指,域名,協(xié)議,端口均相同,需要的朋友可以參考下2023-12-12在Idea2020.1中使用gitee2020.1.0創(chuàng)建第一個代碼庫的實現(xiàn)
這篇文章主要介紹了在Idea2020.1中使用gitee2020.1.0創(chuàng)建第一個代碼庫的實現(xiàn),文中通過圖文示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07xxl-job 帶參數(shù)執(zhí)行和高可用部署方法
這篇文章主要介紹了xxl-job 帶參數(shù)執(zhí)行和高可用部署,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04SpringBoot整合Web開發(fā)之Json數(shù)據(jù)返回的實現(xiàn)
這篇文章主要介紹了SpringBoot整合Web開發(fā)其中Json數(shù)據(jù)返回的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08