小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

深入理解Java內(nèi)存模型(五)——鎖

 wlj2004 2013-12-02

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

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

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

class MonitorExample {
    int a = 0;

    public synchronized void writer() {  //1
        a++;                             //2
    }                                    //3

    public synchronized void reader() {  //4
        int i = a;                       //5
        ……
    }                                    //6
}

假設線程A執(zhí)行writer()方法,隨后線程B執(zhí)行reader()方法。根據(jù)happens before規(guī)則,這個過程包含的happens before 關系可以分為兩類:

  1. 根據(jù)程序次序規(guī)則,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
  2. 根據(jù)監(jiān)視器鎖規(guī)則,3 happens before 4。
  3. 根據(jù)happens before 的傳遞性,2 happens before 5。

上述happens before 關系的圖形化表現(xiàn)形式如下:

在上圖中,每一個箭頭鏈接的兩個節(jié)點,代表了一個happens before 關系。黑色箭頭表示程序順序規(guī)則;橙色箭頭表示監(jiān)視器鎖規(guī)則;藍色箭頭表示組合這些規(guī)則后提供的happens before保證。

上圖表示在線程A釋放了鎖之后,隨后線程B獲取同一個鎖。在上圖中,2 happens before 5。因此,線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個鎖之后,將立刻變得對B線程可見。

鎖釋放和獲取的內(nèi)存語義

當線程釋放鎖時,JMM會把該線程對應的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。以上面的MonitorExample程序為例,A線程釋放鎖后,共享數(shù)據(jù)的狀態(tài)示意圖如下:

當線程獲取鎖時,JMM會把該線程對應的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。下面是鎖獲取的狀態(tài)示意圖:

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

下面對鎖釋放和鎖獲取的內(nèi)存語義做個總結(jié):

  • 線程A釋放一個鎖,實質(zhì)上是線程A向接下來將要獲取這個鎖的某個線程發(fā)出了(線程A對共享變量所做修改的)消息。
  • 線程B獲取一個鎖,實質(zhì)上是線程B接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改的)消息。
  • 線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。

鎖內(nèi)存語義的實現(xiàn)

本文將借助ReentrantLock的源代碼,來分析鎖內(nèi)存語義的具體實現(xiàn)機制。

請看下面的示例代碼:

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;
        ……
    } finally {
        lock.unlock();  //釋放鎖
    }
}
}

在ReentrantLock中,調(diào)用lock()方法獲取鎖;調(diào)用unlock()方法釋放鎖。

ReentrantLock的實現(xiàn)依賴于java同步器框架AbstractQueuedSynchronizer(本文簡稱之為AQS)。AQS使用一個整型的volatile變量(命名為state)來維護同步狀態(tài),馬上我們會看到,這個volatile變量是ReentrantLock內(nèi)存語義實現(xiàn)的關鍵。 下面是ReentrantLock的類圖(僅畫出與本文相關的部分):

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

使用公平鎖時,加鎖方法lock()的方法調(diào)用軌跡如下:

  1. ReentrantLock : lock()
  2. FairSync : lock()
  3. AbstractQueuedSynchronizer : acquire(int arg)
  4. ReentrantLock : tryAcquire(int acquires)

在第4步真正開始加鎖,下面是該方法的源代碼:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();   //獲取鎖的開始,首先讀volatile變量state
    if (c == 0) {
        if (isFirst(current) &&
            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)用軌跡如下:

  1. ReentrantLock : unlock()
  2. AbstractQueuedSynchronizer : release(int arg)
  3. Sync : tryRelease(int releases)

在第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);
    }
    setState(c);           //釋放鎖的最后,寫volatile變量state
    return free;
}

從上面的源代碼我們可以看出,在釋放鎖的最后寫volatile變量state。

公平鎖在釋放鎖的最后寫volatile變量state;在獲取鎖時首先讀這個volatile變量。根據(jù)volatile的happens-before規(guī)則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量后將立即變的對獲取鎖的線程可見。

現(xiàn)在我們分析非公平鎖的內(nèi)存語義的實現(xiàn)。

非公平鎖的釋放和公平鎖完全一樣,所以這里僅僅分析非公平鎖的獲取。

使用公平鎖時,加鎖方法lock()的方法調(diào)用軌跡如下:

  1. ReentrantLock : lock()
  2. NonfairSync : lock()
  3. AbstractQueuedSynchronizer : compareAndSetState(int expect, int update)

在第3步真正開始加鎖,下面是該方法的源代碼:

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

該方法以原子操作的方式更新state變量,本文把java的compareAndSet()方法調(diào)用簡稱為CAS。JDK文檔對該方法的說明如下:如果當前狀態(tài)值等于預期值,則以原子方式將同步狀態(tài)設置為給定的更新值。此操作具有 volatile 讀和寫的內(nèi)存語義。

這里我們分別從編譯器和處理器的角度來分析,CAS如何同時具有volatile讀和volatile寫的內(nèi)存語義。

前文我們提到過,編譯器不會對volatile讀與volatile讀后面的任意內(nèi)存操作重排序;編譯器不會對volatile寫與volatile寫前面的任意內(nèi)存操作重排序。組合這兩個條件,意味著為了同時實現(xiàn)volatile讀和volatile寫的內(nèi)存語義,編譯器不能對CAS與CAS前面和后面的任意內(nèi)存操作重排序。

下面我們來分析在常見的intel x86處理器中,CAS是如何同時具有volatile讀和volatile寫的內(nèi)存語義的。

下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

可以看到這是個本地方法調(diào)用。這個本地方法在openjdk中依次調(diào)用的c++代碼為:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實現(xiàn)在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(對應于windows操作系統(tǒng),X86處理器)。下面是對應于intel x86處理器的源代碼的片段:

// 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ù)當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內(nèi)的順序一致性,不需要lock前綴提供的內(nèi)存屏障效果)。

intel的手冊對lock前綴的說明如下:

  1. 確保對內(nèi)存的讀-改-寫操作原子執(zhí)行。在Pentium及Pentium之前的處理器中,帶有l(wèi)ock前綴的指令在執(zhí)行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內(nèi)存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優(yōu)化:如果要訪問的內(nèi)存區(qū)域(area of memory)在lock前綴指令執(zhí)行期間已經(jīng)在處理器內(nèi)部的緩存中被鎖定(即包含該內(nèi)存區(qū)域的緩存行當前處于獨占或以修改狀態(tài)),并且該內(nèi)存區(qū)域被完全包含在單個緩存行(cache line)中,那么處理器將直接執(zhí)行該指令。由于在指令執(zhí)行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內(nèi)存區(qū)域,因此能保證指令執(zhí)行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執(zhí)行開銷,但是當多處理器之間的競爭程度很高或者指令訪問的內(nèi)存地址未對齊時,仍然會鎖住總線。
  2. 禁止該指令與之前和之后的讀和寫指令重排序。
  3. 把寫緩沖區(qū)中的所有數(shù)據(jù)刷新到內(nèi)存中。

上面的第2點和第3點所具有的內(nèi)存屏障效果,足以同時實現(xiàn)volatile讀和volatile寫的內(nèi)存語義。

經(jīng)過上面的這些分析,現(xiàn)在我們終于能明白為什么JDK文檔說CAS同時具有volatile讀和volatile寫的內(nèi)存語義了。

現(xiàn)在對公平鎖和非公平鎖的內(nèi)存語義做個總結(jié):

  • 公平鎖和非公平鎖釋放時,最后都要寫一個volatile變量state。
  • 公平鎖獲取時,首先會去讀這個volatile變量。
  • 非公平鎖獲取時,首先會用CAS更新這個volatile變量,這個操作同時具有volatile讀和volatile寫的內(nèi)存語義。

從本文對ReentrantLock的分析可以看出,鎖釋放-獲取的內(nèi)存語義的實現(xiàn)至少有下面兩種方式:

  1. 利用volatile變量的寫-讀所具有的內(nèi)存語義。
  2. 利用CAS所附帶的volatile讀和volatile寫的內(nèi)存語義。

concurrent包的實現(xiàn)

由于java的CAS同時具有 volatile 讀和volatile寫的內(nèi)存語義,因此Java線程之間的通信現(xiàn)在有了下面四種方式:

  1. A線程寫volatile變量,隨后B線程讀這個volatile變量。
  2. A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量。
  3. A線程用CAS更新一個volatile變量,隨后B線程用CAS更新這個volatile變量。
  4. A線程用CAS更新一個volatile變量,隨后B線程讀這個volatile變量。

Java的CAS會使用現(xiàn)代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內(nèi)存執(zhí)行讀-改-寫操作,這是在多處理器中實現(xiàn)同步的關鍵(從本質(zhì)上來說,能夠支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,因此任何現(xiàn)代的多處理器都會去支持某種能對內(nèi)存執(zhí)行原子性讀-改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現(xiàn)線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現(xiàn)的基石。如果我們仔細分析concurrent包的源代碼實現(xiàn),會發(fā)現(xiàn)一個通用化的實現(xiàn)模式:

  1. 首先,聲明共享變量為volatile;
  2. 然后,使用CAS的原子條件更新來實現(xiàn)線程之間的同步;
  3. 同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實現(xiàn)線程之間的通信。

AQS,非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現(xiàn)的,而concurrent包中的高層類又是依賴于這些基礎類來實現(xiàn)的。從整體來看,concurrent包的實現(xiàn)示意圖如下:

參考文獻

  1.         Concurrent Programming in Java: Design Principles and Pattern

  2.         JSR 133 (Java Memory Model) FAQ

  3.         JSR-133: Java Memory Model and Thread Specification

  4.         Java Concurrency in Practice

  5.         Java? Platform, Standard Edition 6 API Specification

  6.         The JSR-133 Cookbook for Compiler Writers

  7.         Intel? 64 and IA-32 ArchitecturesvSoftware Developer’s Manual Volume 3A: System Programming Guide, Part 1

  8.         The Art of Multiprocessor Programming

關于作者

程曉明,Java軟件工程師,國家認證的系統(tǒng)分析師、信息項目管理師。專注于并發(fā)編程,就職于富士通南大。個人郵箱:http://www./cn/articles/mailto:asst2003@163.com。

告訴我們您的想法

cas和volatile關系 三月 19, 2013 09:59by fatmind fatmind

感謝作者,幫我把之前零散的知識串起來。

我理解:cas包括兩部分
1.lock:保證具體‘volatile內(nèi)存語義’
2.cmpxchg:保證原子操作

-> lock是volatile在intel平臺的具體實現(xiàn),對嗎 ?
-> 若上述不成了,是說cas自身就具體1、2特性,若去掉‘private volatile int state;’volatile,是否仍滿足 ?

引用“從整體來看,concurrent包的實現(xiàn)示意圖如下”,是把‘volatile’和‘cas’是放在同一層。

謝謝

Re: cas和volatile關系 三月 20, 2013 12:12by 程 曉明

引用“從整體來看,concurrent包的實現(xiàn)示意圖如下”,是把‘volatile’和‘cas’是放在同一層。
--把‘volatile的讀/寫’和‘CAS’是放在同一層,是想回應第一章提到的:“并發(fā)編程中,我們需要處理兩個關鍵問題:線程之間如何通信及線程之間如何同步”
java的concurrent包通過volatile的讀/寫,及CAS所具有的volatile讀和volatile寫的內(nèi)存語義,來實現(xiàn)線程之間的通信。
java的concurrent包使用CAS來實現(xiàn)線程之間的同步。

鎖與共享內(nèi)存的疑問 三月 20, 2013 03:08by p hk

1.“當線程獲取鎖時,JMM會把該線程對應的本地內(nèi)存置為無效。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量?!?br>2.“當線程釋放鎖時,JMM會把該線程對應的本地內(nèi)存中的共享變量刷新到主內(nèi)存中?!?br>
從文中提到的這2點,我的理解是這樣的:
在臨界區(qū)內(nèi):共享變量只能從主內(nèi)存中讀??;共享變量的寫操作只能在本地內(nèi)存中進行,因為在離開臨界區(qū)的時候本地內(nèi)存中的共享變量需要被刷新到主內(nèi)存中,如果寫操作直接是寫入主內(nèi)存,那么最終會因為本地內(nèi)存和主內(nèi)存中的共享變量不一致而導致主內(nèi)存中的共享變量沒有得到應有的更新。

另外我有一個疑問:將本地內(nèi)存的共享變量刷新到主內(nèi)存中的時候,是刷新本地內(nèi)存中的所有共享變量還是只刷新那些發(fā)生過寫操作的共享變量?

Re: 鎖與共享內(nèi)存的疑問 三月 23, 2013 11:03by 程 曉明

--離開臨界區(qū)的時候本地內(nèi)存中的共享變量需要被刷新到主內(nèi)存中.
這是鎖內(nèi)存語義的需要。
如果當前線程A在離開臨界區(qū)時(釋放鎖時),還不把本地內(nèi)存鐘的共享變量刷新到主內(nèi)存;
那么接下來獲取這個鎖的線程B,將無法讀到線程A對共享變量所做的修改。
因此,“離開臨界區(qū)的時候,本地內(nèi)存中的共享變量需要被刷新到主內(nèi)存中”,是鎖通信機制(鎖內(nèi)存語義)的需要。

--是刷新本地內(nèi)存中的所有共享變量還是只刷新那些發(fā)生過寫操作的共享變量?
個人覺得,可以理解為只刷新那些發(fā)生過寫操作的共享變量。

“本地內(nèi)存,主內(nèi)存,從主內(nèi)存中讀取變量,刷新共享變量到主內(nèi)存”,這些都是為了易于讀者理解而虛構(gòu)出來的。事實上,與JSR-133內(nèi)存模型相關的兩大規(guī)范都沒有提到這些概念:
1:《JSR-133: JavaTM Memory Model and Thread Specification》
2:《The Java? Language Specification Third Edition》的“17.4 Memory Model”
與JSR-133內(nèi)存模型相關的著作中,映像中好像只有Brian Goetz在下面這篇文章中提到了這些概念:
http://www.ibm.com/developerworks/java/library/j-jtp03304/index.html
個人覺得這些概念形象,生動,易于讀者理解。所以就把它們借鑒到本文中來了:)
但我個人感覺,使用這些概念也會有弊端。所以本文中,我只是簡單提到它們。

對于JSR-133內(nèi)存模,重點應該關注happens before ,以及編譯器重排序規(guī)則和處理器內(nèi)存屏障插入策略。

對鎖之前如何保證數(shù)據(jù)同步的問題? 十一月 22, 2013 10:08by 秦 軍

非常感謝作者的這一系列文章,收貨蠻大。有一個疑問,在線程中對一個對象同步之前,如何保證線程內(nèi)的本地內(nèi)存寫到主內(nèi)存中呢?因為看了文章,同步鎖進入前,本地內(nèi)存會無效,那么我對一個變量操作了,會寫到主線程嗎?eg :
Obj obj ;
obj.x = 10;

synchronized (obj) {
// ...
}

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多