出自
> [ 深入理解Java內存模型(三)——順序一致性](http://www.infoq.com/cn/articles/java-memory-model-3)
[TOC=1,2]
## 數據競爭與順序一致性保證
當程序未正確同步時,就會存在數據競爭。java內存模型規范對數據競爭的定義如下:
* 在一個線程中寫一個變量,
* 在另一個線程讀同一個變量,
* 而且寫和讀沒有通過同步來排序。
當代碼中包含數據競爭時,程序的執行往往產生違反直覺的結果(前一章的示例正是如此)。如果一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。
JMM對正確同步的多線程程序的內存一致性做了如下保證:
* 如果程序是正確同步的,程序的執行將具有順序一致性(sequentially consistent)--即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同(馬上我們將會看到,這對于程序員來說是一個極強的保證)。這里的同步是指廣義上的同步,包括對常用同步原語(lock,volatile和final)的正確使用。
## 順序一致性內存模型
順序一致性內存模型是一個被計算機科學家理想化了的理論參考模型,它為程序員提供了極強的內存可見性保證。順序一致性內存模型有兩大特性:
* 一個線程中的所有操作必須按照程序的順序來執行。
* (不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。
順序一致性內存模型為程序員提供的視圖如下:

在概念上,順序一致性模型有一個單一的全局內存,這個內存通過一個左右擺動的開關可以連接到任意一個線程。同時,每一個線程必須按程序的順序來執行內存讀/寫操作。從上圖我們可以看出,在任意時間點最多只能有一個線程可以連接到內存。當多個線程并發執行時,圖中的開關裝置能把所有線程的所有內存讀/寫操作串行化。
為了更好的理解,下面我們通過兩個示意圖來對順序一致性模型的特性做進一步的說明。
假設有兩個線程A和B并發執行。其中A線程有三個操作,它們在程序中的順序是:A1->A2->A3。B線程也有三個操作,它們在程序中的順序是:B1->B2->B3。
假設這兩個線程使用監視器來正確同步:A線程的三個操作執行后釋放監視器,隨后B線程獲取同一個監視器。那么程序在順序一致性模型中的執行效果將如下圖所示:

現在我們再假設這兩個線程沒有做同步,下面是這個未同步程序在順序一致性模型中的執行示意圖:

未同步程序在順序一致性模型中雖然整體執行順序是無序的,但所有線程都只能看到一個一致的整體執行順序。以上圖為例,線程A和B看到的執行順序都是:B1->A1->A2->B2->A3->B3。之所以能得到這個保證是因為順序一致性內存模型中的每個操作必須立即對任意線程可見。
但是,在JMM中就沒有這個保證。未同步程序在JMM中不但整體的執行順序是無序的,而且所有線程看到的操作執行順序也可能不一致。比如,在當前線程把寫過的數據緩存在本地內存中,且還沒有刷新到主內存之前,這個寫操作僅對當前線程可見;從其他線程的角度來觀察,會認為這個寫操作根本還沒有被當前線程執行。只有當前線程把本地內存中寫過的數據刷新到主內存之后,這個寫操作才能對其他線程可見。在這種情況下,當前線程和其它線程看到的操作執行順序將不一致。
## 同步程序的順序一致性效果
下面我們對前面的示例程序ReorderExample用監視器來同步,看看正確同步的程序如何具有順序一致性。
請看下面的示例代碼:
~~~
class SynchronizedExample {
int a = 0;
boolean flag = false;
public synchronized void writer() {
a = 1;
flag = true;
}
public synchronized void reader() {
if (flag) {
int i = a;
……
}
}
}
~~~
上面示例代碼中,假設A線程執行writer()方法后,B線程執行reader()方法。這是一個正確同步的多線程程序。根據JMM規范,該程序的執行結果將與該程序在順序一致性模型中的執行結果相同。下面是該程序在兩個內存模型中的執行時序對比圖:

在順序一致性模型中,所有操作完全按程序的順序串行執行。而在JMM中,臨界區內的代碼可以重排序(但JMM不允許臨界區內的代碼“逸出”到臨界區之外,那樣會破壞監視器的語義)。JMM會在退出監視器和進入監視器這兩個關鍵時間點做一些特別處理,使得線程在這兩個時間點具有與順序一致性模型相同的內存視圖(具體細節后文會說明)。雖然線程A在臨界區內做了重排序,但由于監視器的互斥執行的特性,這里的線程B根本無法“觀察”到線程A在臨界區內的重排序。這種重排序既提高了執行效率,又沒有改變程序的執行結果。
從這里我們可以看到JMM在具體實現上的基本方針:在不改變(正確同步的)程序執行結果的前提下,盡可能的為編譯器和處理器的優化打開方便之門。
## 未同步程序的執行特性
對于未同步或未正確同步的多線程程序,JMM只提供最小安全性:線程執行時讀取到的值,要么是之前某個線程寫入的值,要么是默認值(0,null,false),JMM保證線程讀操作讀取到的值不會無中生有(out of thin air)的冒出來。為了實現最小安全性,JVM在堆上分配對象時,首先會清零內存空間,然后才會在上面分配對象(JVM內部會同步這兩個操作)。因此,在以清零的內存空間(pre-zeroed memory)分配對象時,域的默認初始化已經完成了。
JMM不保證未同步程序的執行結果與該程序在順序一致性模型中的執行結果一致。因為未同步程序在順序一致性模型中執行時,整體上是無序的,其執行結果無法預知。保證未同步程序在兩個模型中的執行結果一致毫無意義。
和順序一致性模型一樣,未同步程序在JMM中的執行時,整體上也是無序的,其執行結果也無法預知。同時,未同步程序在這兩個模型中的執行特性有下面幾個差異:
1. 順序一致性模型保證單線程內的操作會按程序的順序執行,而JMM不保證單線程內的操作會按程序的順序執行(比如上面正確同步的多線程程序在臨界區內的重排序)。這一點前面已經講過了,這里就不再贅述。
2. 順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序。這一點前面也已經講過,這里就不再贅述。
3. JMM不保證對64位的long型和double型變量的讀/寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性。
第3個差異與處理器總線的工作機制密切相關。在計算機中,數據通過總線在處理器和內存之間傳遞。每次處理器和內存之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(bus transaction)。總線事務包括讀事務(read transaction)和寫事務(write transaction)。讀事務從內存傳送數據到處理器,寫事務從處理器傳送數據到內存,每個事務會讀/寫內存中一個或多個物理上連續的字。這里的關鍵是,總線會同步試圖并發使用總線的事務。在一個處理器執行總線事務期間,總線會禁止其它所有的處理器和I/O設備執行內存的讀/寫。下面讓我們通過一個示意圖來說明總線的工作機制:

如上圖所示,假設處理器A,B和C同時向總線發起總線事務,這時總線仲裁(bus arbitration)會對競爭作出裁決,這里我們假設總線在仲裁后判定處理器A在競爭中獲勝(總線仲裁會確保所有處理器都能公平的訪問內存)。此時處理器A繼續它的總線事務,而其它兩個處理器則要等待處理器A的總線事務完成后才能開始再次執行內存訪問。假設在處理器A執行總線事務期間(不管這個總線事務是讀事務還是寫事務),處理器D向總線發起了總線事務,此時處理器D的這個請求會被總線禁止。
總線的這些工作機制可以把所有處理器對內存的訪問以串行化的方式來執行;在任意時間點,最多只能有一個處理器能訪問內存。這個特性確保了單個總線事務之中的內存讀/寫操作具有原子性。
在一些32位的處理器上,如果要求對64位數據的讀/寫操作具有原子性,會有比較大的開銷。為了照顧這種處理器,java語言規范鼓勵但不強求JVM對64位的long型變量和double型變量的讀/寫具有原子性。當JVM在這種處理器上運行時,會把一個64位long/ double型變量的讀/寫操作拆分為兩個32位的讀/寫操作來執行。這兩個32位的讀/寫操作可能會被分配到不同的總線事務中執行,此時對這個64位變量的讀/寫將不具有原子性。
當單個內存操作不具有原子性,將可能會產生意想不到后果。請看下面示意圖:

如上圖所示,假設處理器A寫一個long型變量,同時處理器B要讀這個long型變量。處理器A中64位的寫操作被拆分為兩個32位的寫操作,且這兩個32位的寫操作被分配到不同的寫事務中執行。同時處理器B中64位的讀操作被拆分為兩個32位的讀操作,且這兩個32位的讀操作被分配到同一個的讀事務中執行。當處理器A和B按上圖的時序來執行時,處理器B將看到僅僅被處理器A“寫了一半“的無效值。
## 參考文獻
1. [JSR-133: Java Memory Model and Thread Specification](http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf)
2. [Shared memory consistency models: A tutorial](http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf)
3. [The JSR-133 Cookbook for Compiler Writers](http://gee.cs.oswego.edu/dl/jmm/cookbook.html)
4. [深入理解計算機系統(原書第2版)](http://www.amazon.cn/%E5%A4%A7%E6%95%B0%E6%8D%AE%E6%8A%80%E6%9C%AF%C2%B7%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6%E4%B8%9B%E4%B9%A6-%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%B3%BB%E7%BB%9F-%E5%B8%83%E8%8E%B1%E6%81%A9%E7%89%B9/dp/B004BJ18KM/ref=sr_1_1?s=books&ie=UTF8&qid=1353940736&sr=1-1)
5. [UNIX Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers](http://www.amazon.com/exec/obidos/tg/detail/-/0201633388/qid=1053590503/sr=8-1/ref=sr_8_1/104-7262980-9184702?v=glance&s=books&n=507846)
6. [The Java Language Specification, Third Edition](http://docs.oracle.com/javase/specs/jls/se5.0/jls3.pdf)
## 作者簡介
程曉明,Java軟件工程師,國家認證的系統分析師、信息項目管理師。專注于并發編程,就職于富士通南大。個人郵箱:[asst2003@163.com](mailto:asst2003@163.com)。
* * *
感謝[張龍](http://www.infoq.com/cn/bycategory.action?authorName=%E5%BC%A0%E9%BE%99)對本文的審校。
給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至[editors@cn.infoq.com](mailto:editors@cn.infoq.com)。也歡迎大家通過新浪微博([@InfoQ](http://www.weibo.com/infoqchina))或者騰訊微博([@InfoQ](http://t.qq.com/infoqchina))關注我們,并與我們的編輯和其他讀者朋友交流。
- 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認知(包括框架圖、詳細介紹、示例說明)