今天來聊一聊Java併發編程中兩個經常使用的關鍵字:volatile和synchronized。在介紹這兩個關鍵字以前,首先要搞明白併發編程中的兩個問題:java
線程之間是如何通訊的程序員
線程之間如何同步編程
Java內存模型
Java線程的通訊由Java內存模型(JMM)控制,Java內存模型的抽象如圖:微信
Java線程之間的通訊老是隱式進行,通訊過程對程序員徹底透明。多個線程經過讀-寫共享內存來實現通訊。網絡
圖中線程A與線程B通訊的具體步驟是:多線程
線程A把更新過的共享變量刷新到主內存中併發
線程B從主內存讀取共享變量ide
例如,共享變量x的初始值爲0,線程A將x修改成1(x=x+1),線程B讀取到的x就是1,對於程序員來說,就是線程A給線程B發消息說它把x的值更新爲1。性能
第一個問題搞明白了,再思考一下第二個問題。線程之間如何同步?在併發編程中,有三個重要的概念:原子性、可見性、一致性。atom
原子性
在Java中,對基本數據類型的讀取和賦值操做都屬於原子操做。
x = 10;
x = x + 1;
上面兩條語句中,第一句是原子操做,而第二句不是,爲何呢?實際上,第二句代碼被編譯爲3條指令:
從內存中取x的值
x+1操做
計算結果存入內存
可見性
當多個線程訪問同一變量時,若是有一個線程修改了這個變量,那麼其餘線程馬上能夠看到修改後的值。
有序性
CPU執行指令是按照前後順序執行的,可是指令的順序並不必定等同於代碼的順序,編譯器編譯過程當中,爲了提升性能,經常進行指令重排序。這種重排序不會改變單線程的語義,也就是說,你寫的一段代碼若是是單線程執行,編譯器可能對執行進行重排序,但不論如何排序,最後獲得的結果都是相同的。
另外,若是存在數據依賴性,編譯器不會改變依賴關係的執行順序。數據依賴性是指兩個操做訪問同一個變量,其中一個是寫操做,那麼這兩個操做就有數據依賴性。
重排序對應多線程有哪些影響呢,咱們經過一段代碼來看一下:
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
Public void reader() {
if (flag) { // 3
int i = a * a; // 4
……
}
}
}
上述代碼中,flag是變量a被初始化的標識,若是此時有兩個線程A和B,A執行writer()方法,B執行reader()方法。因爲1和二、3和4不存在數據依賴性,那麼就有可能出現這種狀況:
A先執行語句2
B執行了語句3和4
A執行語句1
最終的結果並非咱們想要的,此時,重排序破壞了語義。
線程同步
對於上面所說的線程同步問題如何避免呢?可使用Java中的volatile和synchronized這兩個關鍵字。
volatile
volatile關鍵字比較輕量級,只能夠修飾變量。volatile修飾的變量,若是值被更新,會當即刷新主內存,而讀volatile修飾的變量時,JMM會把線程對應的本地內存置爲無效,從主內存中讀取。這樣volatile就能夠保證線程的可見性。
volatile關鍵字在必定程度上能夠保證有序性:
當第二個操做是volatile寫時,不能進行重排序
當第一個操做是volatile讀時,不能進行重排序
當第一個操做是volatile寫,第二個操做是volatile讀時,不能重排序
爲了實現這些語義,JMM採用屏障插入策略:
在volatile寫操做前插入StoreStore屏障,後面插入StoreLoad屏障
在volatile讀操做後面插入LoadLoad屏障和LoadStore屏障
也就是說,volatile寫操做前的全部寫操做都必須執行完,且須要等到volatile寫操做執行後才能執行讀操做。volatile讀操做執行完以後才能夠進行其餘操做。
能夠把volatile當作一個屏障,其前面的操做不能放到volatile操做後面,後面的操做也不能放到volatile操做前面。
synchronized
synchronized比較重量級,能夠用來修飾方法。synchronized關鍵字是給修飾對象加鎖,只有得到鎖的線程才能夠執行,執行完後釋放鎖。所以synchronized保證了原子性和可見性。
本文分享自微信公衆號 - 代碼潔癖患者(Jackeyzhe2018)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。