- 你有一個思想,我有一個思想,咱們交換後,一我的就有兩個思想
- If you can NOT explain it simply, you do NOT understand it well enough
現陸續將Demo代碼和技術文章整理在一塊兒 Github實踐精選 ,方便你們閱讀查看,本文一樣收錄在此,以爲不錯,還請Starhtml
以前寫了幾篇 Java併發編程的系列 文章,有個朋友微羣裏問我,仍是不能理解 volatile
和 synchronized
兩者的區別, 他的問題主要能夠概括爲這幾個:java
若是你不能回答上面的幾個問題,說明你對兩者的區別還有一些含混。本文就經過圖文的方式好好說說他們微妙的關係git
都聽過【天上一天,地下一年】,假設 CPU 執行一條普通指令須要一天,那麼 CPU 讀寫內存就得等待一年的時間。github
受【木桶原理】的限制,在CPU眼裏,程序的總體性能都被內存的辦事效率拉低了,爲了解決這個短板,硬件同窗也使用了咱們作軟件經常使用的提速策略——使用緩存Cache(實則是硬件同窗給軟件同窗挖的坑)面試
CPU 增長了緩存均衡了與內存的速度差別,這一增長仍是好幾層。算法
此時內存的短板再也不那麼明顯,CPU甚喜。但隨之卻帶來不少問題編程
看上圖,每一個核都有本身的一級緩存(L1 Cache),有的架構裏面還有全部核共用的二級緩存(L2 Cache)。使用緩存以後,當線程要訪問共享變量時,若是 L1 中存在該共享變量,就不會再逐級訪問直至主內存了。因此,經過這種方式,就補上了訪問內存慢的短板segmentfault
具體來講,線程讀/寫共享變量的步驟是這樣:緩存
假設如今主內存中有共享變量 X, 其初始值爲 0安全
線程1先訪問變量 X, 套用上面的步驟就是這樣:
此時,在線程 1 眼中,X 的值是這樣的:
接下來,線程 2 一樣按照上面的步驟訪問變量 X
此時,線程 2 眼中,X 的值是這樣的:
結合剛剛的兩次操做,當線程1再訪問變量x,咱們看看有什麼問題:
此刻,若是線程 1 再次將 x=1回寫,就會覆蓋線程2 x=2 的結果,一樣的共享變量,線程拿到的結果卻不同(線程1眼中x=1;線程2眼中x=2),這就是共享變量內存不可見的問題。
怎麼補坑呢?今天的兩位主角閃亮登場,不過在說明 volatile關鍵字以前,咱們先來講說你最熟悉的 synchronized 關鍵字
遇到線程不安全的問題,習慣性的會想到用 synchronized 關鍵字來解決問題,暫且先不論該辦法是否合理,咱們來看 synchronized 關鍵字是怎麼解決上面提到的共享變量內存可見性問題的
二話不說,無情向下看 volatile
當一個變量被聲明爲 volatile 時:
有種換湯不換藥的感受,你看的一點都沒錯
因此,當使用 synchronized 或 volatile 後,多線程操做共享變量的步驟就變成了這樣:
簡單點來講就是再也不參考 L1 和 L2 中共享變量的值,而是直接訪問主內存
來點踏實的,上例子
public class ThreadNotSafeInteger { /** * 共享變量 value */ private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
通過前序分析鋪墊,很明顯,上面代碼中,共享變量 value 存在大大的隱患,嘗試對其做出一些改變
先使用 volatile 關鍵字改造:
public class ThreadSafeInteger { /** * 共享變量 value */ private volatile int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
再使用 synchronized 關鍵字改造
public class ThreadSafeInteger { /** * 共享變量 value */ private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
這兩個結果是徹底相同,在解決【當前】共享變量數據可見性的問題上,兩者算是等同的
若是說 synchronized 和 volatile 是徹底等同的,那就不必設計兩個關鍵字了,繼續看個例子
@Slf4j public class VisibilityIssue { private static final int TOTAL = 10000; // 即使像下面這樣加了 volatile 關鍵字修飾不會解決問題,由於並無解決原子性問題 private volatile int count; public static void main(String[] args) { VisibilityIssue visibilityIssue = new VisibilityIssue(); Thread thread1 = new Thread(() -> visibilityIssue.add10KCount()); Thread thread2 = new Thread(() -> visibilityIssue.add10KCount()); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { log.error(e.getMessage()); } log.info("count 值爲:{}", visibilityIssue.count); } private void add10KCount(){ int start = 0; while (start ++ < TOTAL){ this.count ++; } } }
其實就是將上面setValue 簡單賦值操做 (this.value = value;)變成了 (this.count ++;)形式,若是你運行代碼,你會發現,count的值始終是處於1w和2w之間的
將上面方法再以 synchronized 的形式作改動
@Slf4j public class VisibilityIssue { private static final int TOTAL = 10000; private int count; //... 同上 private synchronized void add10KCount(){ int start = 0; while (start ++ < TOTAL){ this.count ++; } } }
再次運行代碼,count 結果就是 2w
兩組代碼,都經過 volatile 和 synchronized 關鍵字以一樣形式修飾,怎麼有的能夠帶來相同結果,有的卻不能呢?
這就要說說兩者的不一樣了
count++ 程序代碼是一行,可是翻譯成 CPU 指令確是三行( 不信你用
javap -c
命令試試)
synchronized 是獨佔鎖/排他鎖(就是有你沒個人意思),同時只能有一個線程調用 add10KCount
方法,其餘調用線程會被阻塞。因此三行 CPU 指令都是同一個線程執行完以後別的線程才能繼續執行,這就是一般說說的 原子性 (線程執行多條指令不被中斷)
但 volatile 是非阻塞算法(也就是不排他),當遇到三行 CPU 指令天然就不能保證別的線程不插足了,這就是一般所說的,volatile 能保證內存可見性,可是不能保證原子性
一句話,那何時才能用volatile關鍵字呢?(千萬記住了,重要事情說三遍,感受這句話過期了)
若是寫入變量值不依賴變量當前值,那麼就能夠用 volatile
若是寫入變量值不依賴變量當前值,那麼就能夠用 volatile
若是寫入變量值不依賴變量當前值,那麼就能夠用 volatile
好比上面 count++ ,是獲取-計算-寫入三步操做,也就是依賴當前值的,因此不能靠volatile 解決問題
到這裏,文章開頭第一個問題【volatile 與 synchronized 在處理哪些問題是相對等價的?】答案已經揭曉了
先本身腦補一下,若是讓你同一段時間內【寫幾行代碼】就要去【數錢】,數幾下錢就要去【唱歌】,唱完歌又要去【寫代碼】,反覆頻繁這樣操做,還要接上上一次的操做(代碼接着寫,錢累加着數,歌接着唱)還須要保證不出錯,你累不累?
synchronized 是排他的,線程排隊就要有切換,這個切換就比如上面的例子,要完成切換,還得記準線程上一次的操做,很累CPU大腦,這就是一般說的上下文切換會帶來很大開銷
volatile 就不同了,它是非阻塞的方式,因此在解決共享變量可見性問題的時候,volatile 就是 synchronized 的弱同步體現了
到這,文章的第二個問題【爲何說 volatile 是 synchronized 弱同步的方式?】你也應該明白了吧
volatile 除了還能解決可見性問題,還能解決編譯優化重排序問題,以前的文章已經介紹過,請你們點擊連接自行查看就好(面試常問的雙重檢查鎖單例模式爲何不是線程安全的也能夠在裏面找到答案哦):
看完這兩篇文章,相信第三個問題也就迎刃而解了
瞭解了這些,相信你也就懂得如何使用了
精挑細選,終於整理完第一版 Java 技術棧硬核資料,搶先看就私信回覆【資料】/【666】吧
下一篇文章,咱們來講說【喚醒線程爲何建議用notifyAll而不建議用notify呢?】
我的博客:https://dayarch.top
歡迎關注個人公衆號 「日拱一兵」,趣味原創解析Java技術棧問題,將複雜問題簡單化,將抽象問題圖形化落地
若是對個人專題內容感興趣,或搶先看更多內容,歡迎訪問個人博客 dayarch.top