出自
> [深入理解Java內存模型(五)——鎖](http://www.infoq.com/cn/articles/java-memory-model-5)
[TOC=1,2]
## 鎖的釋放-獲取建立的happens before 關系
鎖是java并發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發送消息。
下面是鎖釋放-獲取的示例代碼:
~~~
class MonitorExample {
int a = 0;
public synchronized void writer() { //1
a++; //2
} //3
public synchronized void reader() { //4
int i = a; //5
……
} //6
}
~~~
假設線程A執行writer()方法,隨后線程B執行reader()方法。根據happens before規則,這個過程包含的happens before 關系可以分為兩類:
1. 根據程序次序規則,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
2. 根據監視器鎖規則,3 happens before 4。
3. 根據happens before 的傳遞性,2 happens before 5。
上述happens before 關系的圖形化表現形式如下:

在上圖中,每一個箭頭鏈接的兩個節點,代表了一個happens before 關系。黑色箭頭表示程序順序規則;橙色箭頭表示監視器鎖規則;藍色箭頭表示組合這些規則后提供的happens before保證。
上圖表示在線程A釋放了鎖之后,隨后線程B獲取同一個鎖。在上圖中,2 happens before 5。因此,線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個鎖之后,將立刻變得對B線程可見。
## 鎖釋放和獲取的內存語義
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。以上面的MonitorExample程序為例,A線程釋放鎖后,共享數據的狀態示意圖如下:

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

對比鎖釋放-獲取的內存語義與volatile寫-讀的內存語義,可以看出:鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。
下面對鎖釋放和鎖獲取的內存語義做個總結:
* 線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。
* 線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。
* 線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。
## 鎖內存語義的實現
本文將借助ReentrantLock的源代碼,來分析鎖內存語義的具體實現機制。
請看下面的示例代碼:
~~~
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中,調用lock()方法獲取鎖;調用unlock()方法釋放鎖。
ReentrantLock的實現依賴于java同步器框架AbstractQueuedSynchronizer(本文簡稱之為AQS)。AQS使用一個整型的volatile變量(命名為state)來維護同步狀態,馬上我們會看到,這個volatile變量是ReentrantLock內存語義實現的關鍵。 下面是ReentrantLock的類圖(僅畫出與本文相關的部分):

ReentrantLock分為公平鎖和非公平鎖,我們首先分析公平鎖。
使用公平鎖時,加鎖方法lock()的方法調用軌跡如下:
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()的方法調用軌跡如下:
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變量。根據volatile的happens-before規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量后將立即變的對獲取鎖的線程可見。
現在我們分析非公平鎖的內存語義的實現。
非公平鎖的釋放和公平鎖完全一樣,所以這里僅僅分析非公平鎖的獲取。
使用公平鎖時,加鎖方法lock()的方法調用軌跡如下:
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()方法調用簡稱為CAS。JDK文檔對該方法的說明如下:如果當前狀態值等于預期值,則以原子方式將同步狀態設置為給定的更新值。此操作具有 volatile 讀和寫的內存語義。
這里我們分別從編譯器和處理器的角度來分析,CAS如何同時具有volatile讀和volatile寫的內存語義。
前文我們提到過,編譯器不會對volatile讀與volatile讀后面的任意內存操作重排序;編譯器不會對volatile寫與volatile寫前面的任意內存操作重排序。組合這兩個條件,意味著為了同時實現volatile讀和volatile寫的內存語義,編譯器不能對CAS與CAS前面和后面的任意內存操作重排序。
下面我們來分析在常見的intel x86處理器中,CAS是如何同時具有volatile讀和volatile寫的內存語義的。
下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:
~~~
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
~~~
可以看到這是個本地方法調用。這個本地方法在openjdk中依次調用的c++代碼為:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實現在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(對應于windows操作系統,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
}
}
~~~
如上面源代碼所示,程序會根據當前處理器的類型來決定是否為cmpxchg指令添加lock前綴。如果程序是在多處理器上運行,就為cmpxchg指令加上lock前綴(lock cmpxchg)。反之,如果程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不需要lock前綴提供的內存屏障效果)。
intel的手冊對lock前綴的說明如下:
1. 確保對內存的讀-改-寫操作原子執行。在Pentium及Pentium之前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處于獨占或以修改狀態),并且該內存區域被完全包含在單個緩存行(cache line)中,那么處理器將直接執行該指令。由于在指令執行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。這個操作過程叫做緩存鎖定(cache locking),緩存鎖定將大大降低lock前綴指令的執行開銷,但是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。
2. 禁止該指令與之前和之后的讀和寫指令重排序。
3. 把寫緩沖區中的所有數據刷新到內存中。
上面的第2點和第3點所具有的內存屏障效果,足以同時實現volatile讀和volatile寫的內存語義。
經過上面的這些分析,現在我們終于能明白為什么JDK文檔說CAS同時具有volatile讀和volatile寫的內存語義了。
現在對公平鎖和非公平鎖的內存語義做個總結:
* 公平鎖和非公平鎖釋放時,最后都要寫一個volatile變量state。
* 公平鎖獲取時,首先會去讀這個volatile變量。
* 非公平鎖獲取時,首先會用CAS更新這個volatile變量,這個操作同時具有volatile讀和volatile寫的內存語義。
從本文對ReentrantLock的分析可以看出,鎖釋放-獲取的內存語義的實現至少有下面兩種方式:
1. 利用volatile變量的寫-讀所具有的內存語義。
2. 利用CAS所附帶的volatile讀和volatile寫的內存語義。
## concurrent包的實現
由于java的CAS同時具有 volatile 讀和volatile寫的內存語義,因此Java線程之間的通信現在有了下面四種方式:
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會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,因此任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:
1. 首先,聲明共享變量為volatile;
2. 然后,使用CAS的原子條件更新來實現線程之間的同步;
3. 同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。
AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴于這些基礎類來實現的。從整體來看,concurrent包的實現示意圖如下:

## 參考文獻
1. [?Concurrent Programming in Java: Design Principles and Pattern](http://www.amazon.com/Concurrent-Programming-Java-Principles-Pattern/dp/0201310090/ref=sr_1_1?s=books&ie=UTF8&qid=1341416393&sr=1-1&keywords=Concurrent+Programming+in+Java+Design+Principles+and+Patterns)
2. [?JSR 133 (Java Memory Model) FAQ](http://www.cs.umd.edu/users/pugh/java/memoryModel/jsr-133-faq.html)
3. [?JSR-133: Java Memory Model and Thread Specification](http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf)
4. [?Java Concurrency in Practice](http://www.amazon.com/Java-Concurrency-Practice-Brian-Goetz/dp/0321349601/ref=pd_sim_b_1)
5. [?Java? Platform, Standard Edition 6 API Specification](http://docs.oracle.com/javase/6/docs/api/)
6. [?The JSR-133 Cookbook for Compiler Writers](http://gee.cs.oswego.edu/dl/jmm/cookbook.html)
7. [?Intel? 64 and IA-32 ArchitecturesvSoftware Developer’s Manual Volume 3A: System Programming Guide, Part 1](http://download.intel.com/products/processor/manual/253668.pdf)
8. [?The Art of Multiprocessor Programming](http://www.amazon.com/Art-Multiprocessor-Programming-Maurice-Herlihy/dp/0123705916/ref=sr_1_1?ie=UTF8&s=books&qid=1248834374&sr=1-1)
## 關于作者
程曉明,Java軟件工程師,國家認證的系統分析師、信息項目管理師。專注于并發編程,就職于富士通南大。個人郵箱:[asst2003@163.com](mailto:asst2003@163.com)。
【QCon上海2016】說來就來,優秀講師想嗨就嗨,一言不合就國際范兒,Spotify 架構師Igor Maravi?、Twitter 高級工程師黃浩、Netflix 軟件工程師Steven Wu、Vue Technology LLC 創始人、Vue.js 作者尤雨溪等國際咖紛紛上線,就是這個feel,倍兒爽!8月21日前購票即享[8折優惠](http://2016.qconshanghai.com/apply?utm_source=infoq&utm_medium=textads)。
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)