Java內存模型(五)鎖的內存語義

      網友投稿 599 2025-03-31

      簡介:

      鎖的作用是讓臨界區互斥執行。本文闡述所得另一個重要知識點——鎖的內存語義。

      1、鎖的釋放-獲取建立的happens-before關系

      鎖是Java并發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發送消息。

      鎖釋放-獲取的示例代碼:

      package com.lizba.p1; /** *

      * 鎖示例代碼 *

      * * @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; }

      假設線程A執行writer()方法,隨后線程B執行reader()方法。根據happens-before規范,這個過程包含的happens-before關系可以分為3類。

      根據程序次序規則:1 happens-before 2,2 happens-before 3, 4 happens-before 5,5 happens-before 6

      根據監視器鎖規則:3 happens-before 4

      根據happens-before的傳遞性,2 happens-before 5

      上述happens-before關系的圖形化表現形式如圖:

      總結:

      線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個鎖之后,將立即變得對B線程可見。

      2、鎖釋放和獲取的內存語義

      當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。以上述MonitorExample程序為例,A線程釋放鎖后,共享數據的狀態示意圖如下所示:

      共享數據的狀態示意圖

      當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效。從而使得被監視器鎖保護的臨界區代碼必須從主內存中讀取共享變量。

      鎖獲取的狀態示意圖

      對比鎖釋放-獲取鎖的內存語義與volatile寫-讀的內存語義可以看出:鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。

      總結:

      線程A釋放鎖,實質上是線程A向接下來要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。

      線程B獲取鎖,實質上是線程B接受了之前某個線程發出的(在釋放這個鎖對共享變量鎖做的修改的)消息。

      線程A是否鎖,隨后線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。

      3、鎖內存的語義實現

      分析ReentrantLock的源代碼,來分析鎖內存語義的具體實現機制。

      示例代碼:

      package com.lizba.p1; import java.util.concurrent.locks.ReentrantLock; /** *

      * ReentrantLock示例代碼 *

      * * @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中,調用lock()方法獲取鎖;調用unlock()方法釋放鎖。

      ReentrantLock的實現依賴于Java同步器框架AbstractQueuedSynchronized(AQS)。AQS使用一個整型的volatile變量(state)來維護同步狀態,這個volatile變量是ReentrantLock內存語義實現的關鍵。

      ReetrantLock的類圖

      ReentrantLock分為公平鎖和非公平鎖,首先分析公平鎖。

      使用公平鎖時,加鎖方法lock()的調用軌跡如下。

      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()調用軌跡如下:

      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。

      總結公平鎖:

      根據volatile的happens-before規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取到同一個volatile變量后將立即變得對獲取鎖的線程可見。

      現在分析非公平鎖:

      注意,非公平鎖的釋放和公平鎖的釋放完全一致,都是上面的源代碼。所以下面只分析非公平鎖的獲取過程。

      使用非公平鎖,加鎖方法lock()的調用軌跡如下:

      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中實現 return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }

      該方法以原子操作的方式更新state變量,也就是compareAndSet() (CAS)操作。JDK文檔對該方法說明如下:如果當前狀態值等于預期值,則以原子方式同步狀態設置為給定更新的值。此操作具有volatile讀和寫的內存語義。

      接下來分別從編譯器和處理器的角度來分析,CAS如何同時具有volatile讀和volatile寫的內存語義。

      編譯器的角度:

      前文已經講過,編譯器不會對volatile讀與volatile讀后面的任意內存操作重排序;編譯器不會對volatile寫和volatile寫后前面的任意內存操作重排序。組合這兩個條件,意味著同時實現volatile讀和volatile寫的內存語義,編譯器不能對CAS與CAS前面和后面任意內存操作重排序。

      處理器的角度:

      (本人不太懂C++)這一塊總結需要看JVM源碼,可能會總結錯誤,如需要深入理解這一塊請查看《Java并發編程藝術》53頁。

      sun.misc.Unsafe中的compareAndSwapInt源碼如下:(不懂Unsafe請看往期文章)

      public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

      這是一個本地方法。這個本地方法會在openJDK中調用C++代碼,假設當前是X86處理器,程序會根據當前處理器的類型來決定是非cmpxchg指令添加lock前綴。如果:

      程序運行在多處理器上,就為cmpxchg指令加上lock前綴(Lock Cmpxchg)

      程序運行在單處理器上,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)

      intel手冊對lock前綴的說明

      對內存的讀-改-寫操作原子執行。(總線鎖定/緩存鎖定)

      禁止該指令,與之前的讀和寫指令重排序

      把寫緩沖區的所有數據刷新到內存中

      Java內存模型(五)鎖的內存語義

      上面的2、3兩點所具有的內存屏障的效果,足以同時實現volatile讀和volatile寫的內存語義。所以JDK文檔說CAS 具有volatile讀和volatile寫的內存語義對于處理器也是符合的。

      公平鎖和非公平鎖的總結

      公平鎖和非公平鎖的釋放,最后都需要寫一個volatile變量state

      公平鎖獲取時,首先會去讀volatile變量

      非公平鎖獲取鎖時,首先會用CAS更新volatile變量,這個操作同時具有volatile讀和volatile寫的內存語義

      釋放鎖-獲取鎖的內存語義的實現方式總結

      利用volatile變量的寫-讀所具有的內存語義

      利用CAS所附帶的volatile讀和volatile寫的內存語義

      4、concurrent包的實現

      由于Java的CAS同時具有volatile讀和volatile寫的內存語義,因此Java線程之間的通信方式有以下4種方式

      A線程寫volatile變量,隨后B線程讀這個volatile變量

      A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量

      A線程利用CAS更新一個volatile變量,隨后B線程用CAS更新這個volatile變量

      A線程利用CAS更新一個volatile變量,隨后B線程讀這個volatile變量

      Java的CAS會使用現代處理器上提供的高效機器級別的原子指令,這些原子指令以原子方式對內存執行讀-改-寫操作,這是在多處理器實現同步的關鍵。同時volatile變量的讀/寫和CAS可以實現線程之間的通信。這些特性就是Java整個concurrent包的基石。

      concurrent包的通用化實現模式

      聲明共享變量volatile

      使用CAS的原子條件更新來實現線程之間的同步

      配合volatile的讀/寫和CAS具有的volatile讀和寫的內存語義來實現線程之間的通信。

      AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)、非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中基礎類都是使用這個模式來實現的,而concurrent包中的高層類又是依賴于這些基礎類。

      圖示concurrent包的實現示意圖

      concurrent包的實現示意圖

      文章總結至《Java并發編程藝術》,下篇總結“final域的內存語義”,敬請關注。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:word文檔如何設置部分橫向
      下一篇:怎么取消組合(excel怎么取消組合)
      相關文章
      亚洲一区在线免费观看| 亚洲国产成人精品无码区在线秒播| 亚洲一区二区三区免费视频| 亚洲av无码不卡一区二区三区| 亚洲午夜久久久影院| 国内精品久久久久久久亚洲| 国产亚洲精品拍拍拍拍拍| 亚洲综合色在线观看亚洲| 久久久久亚洲AV成人网人人网站| 亚洲精品国产精品国自产观看| 亚洲成A∨人片天堂网无码| 亚洲国产成人精品久久久国产成人一区二区三区综 | 亚洲日韩一区二区一无码| 亚洲最大无码中文字幕| 亚洲一区二区三区国产精华液| 亚洲日本VA中文字幕久久道具| 亚洲精品V天堂中文字幕| 亚洲高清毛片一区二区| 最新亚洲人成无码网站| 亚洲国产精品成人| 中文字幕久久亚洲一区| 亚洲色婷婷一区二区三区| 久久久久亚洲av无码尤物| 亚洲国产国产综合一区首页| 久久亚洲国产精品成人AV秋霞| 亚洲美女视频网址| 亚洲日日做天天做日日谢| 亚洲日韩av无码中文| 国产天堂亚洲国产碰碰| 亚洲午夜激情视频| 国产亚洲人成网站观看| 亚洲国产人成在线观看69网站| 亚洲精品国产福利在线观看| 亚洲人成电影在线观看网| 亚洲性色AV日韩在线观看| 自拍偷自拍亚洲精品播放| 亚洲人成电影网站国产精品 | 亚洲七七久久精品中文国产| 国产中文在线亚洲精品官网| 亚洲AV午夜福利精品一区二区| 亚洲黄色片在线观看|