一、前言 七月的天氣分外炎熱,心中燥意難以撫平,決定梳理下之前落下的筆記。正好最近對JMM比較感興趣,就整理出了這篇文字。 二、序言 即使你的程式沒有顯示的創建任何線程,框架也可能為你創建了一些線程,這些線程調用的代碼必須是線程安全(thread safe)的。這一點給開發人員的設計和實現賦予了更重 ...
一、前言
七月的天氣分外炎熱,心中燥意難以撫平,決定梳理下之前落下的筆記。正好最近對JMM比較感興趣,就整理出了這篇文字。
二、序言
即使你的程式沒有顯示的創建任何線程,框架也可能為你創建了一些線程,這些線程調用的代碼必須是線程安全(thread safe)的。這一點給開發人員的設計和實現賦予了更重要的一分責任。
本文上下兩篇,主要通過圖文結合的方式描述瞭如下五個方面,希望有所幫助:
- Java記憶體模型
- 指令重排序
- 順序一致性模型
- volatile記憶體語義與實現
- 鎖記憶體語義與實現
三、正文
1. Java記憶體模型
在併發編程中,需要處理兩個關鍵的問題:線程之間如何 通信 及線程之間如何 同步。
通信
通信是指線程之間以何種機制來交換信息。在命令式編程語言中,線程之間的通信機制有兩種方法:共用記憶體和消息傳遞。
共用記憶體
這種併發模型中,線程之間通過寫-讀記憶體中的公共狀態來進行隱式通信。Java底層就是採用的這種方式。
消息傳遞
這種併發模型中,線程之間沒有公共狀態,線程之間必須通過發送消息來顯示進行通信。如:流行的Actor模型。
同步
Java線程之間的通信由Java記憶體模型控制(簡稱JMM),JMM決定一個線程對共用變數的寫入,何時對另一個線程可見。
JMM定義了線程和主記憶體之間的抽象關係:線程之間的共用變數存儲在主記憶體中,每個線程都有一個本地記憶體(Local Memory),本地記憶體中存儲了該線程讀/寫共用變數的副本。
由此可見,線程A與線程B要通信的話,必須經歷2個步驟。
a.線程A將本地記憶體中的副本刷寫到主記憶體中去。
b.線程B從主記憶體讀取已被A更新過的共用變數到B的本地記憶體
那麼,只要有其中一個環節沒有完成,線程間的可見性就得不到保障,造成數據的不准確。
2. 指令重排序
2.1 什麼是指令重排序?
在執行程式時,為了提高性能,編譯器和處理器常常會對指令做重排序。
2.2 指令重排序的條件
2.2.1 數據依賴性
如果兩個操作訪問統一個變數,且其中一個為寫操作,那麼這兩個操作之間就存在數據依賴性。
重排序在以上三種情況下不會觸發。
2.2.2 as-if-serial語義
as-if-serial語義的意思是:不管怎麼重排序,(單線程)程式的執行結果不能被改變。
我們通過一個計算圓面積的例子,說明這個問題:
可以看到,無論如何重排序,程式的結果是不變的,準確的。
以上,我們討論了單線程情況下重排序的影響。(好像與我們寫程式期望的目標一致: P),對於多線程程式,就未必如此了。
我們也是通過一個簡單的程式來說明該影響:
假設有兩個線程A和B,A先執行writer方法,然後B執行reader方法。
我們可以分析得到,這兩個方法內的代碼都不符合數據依賴性原則,因此都有可能被重排序。
重排序造成的執行路徑有很多種,我們這裡拿出其中的兩種來說明問題。
讀取到了未賦值的a變數
說明:int i = a * a;
這個操作可以分成兩個步驟,首先計算 a * a,接著賦值給變數i。
大家可以在腦中計算下這兩種情況下變數i的值。顯然,都不符合我們寫代碼期望的結果。我們初步認識到了,重排序對於多線程情況下的可能造成的負面影響。
3. 順序一致性模型
上面我們初步認識到了重排序可能對多線程的負面影響,那麼如何避免此種情況的發生呢?
我們下篇接著討論: D.