volatile和synchronized到底啥區別?多圖文講解告訴你

  • 你有一個思想,我有一個思想,咱們交換後,一我的就有兩個思想html

  • If you can NOT explain it simply, you do NOT understand it well enoughjava

現陸續將Demo代碼和技術文章整理在一塊兒 Github實踐精選 ,方便你們閱讀查看,本文一樣收錄在此,以爲不錯,還請Stargit

以前寫了幾篇 Java併發編程的系列 文章,有個朋友微羣裏問我,仍是不能理解 volatilesynchronized 兩者的區別, 他的問題主要能夠概括爲這幾個:github

  • volatile 與 synchronized 在處理哪些問題是相對等價的?面試

  • 爲何說 volatile 是 synchronized 弱同步的方式?算法

  • volatile 除了可見性問題,還能解決什麼問題?編程

  • 兩者我要如何選擇使用?緩存

若是你不能回答上面的幾個問題,說明你對兩者的區別還有一些含混。本文就經過圖文的方式好好說說他們微妙的關係安全

都聽過【天上一天,地下一年】,假設 CPU 執行一條普通指令須要一天,那麼 CPU 讀寫內存就得等待一年的時間。微信

受【木桶原理】的限制,在CPU眼裏,程序的總體性能都被內存的辦事效率拉低了,爲了解決這個短板,硬件同窗也使用了咱們作軟件經常使用的提速策略——使用緩存Cache(實則是硬件同窗給軟件同窗挖的坑)

Java 內存模型(JMM)

CPU 增長了緩存均衡了與內存的速度差別,這一增長仍是好幾層。

此時內存的短板再也不那麼明顯,CPU甚喜。但隨之卻帶來不少問題

看上圖,每一個核都有本身的一級緩存(L1 Cache),有的架構裏面還有全部核共用的二級緩存(L2 Cache)。使用緩存以後,當線程要訪問共享變量時,若是 L1 中存在該共享變量,就不會再逐級訪問直至主內存了。因此,經過這種方式,就補上了訪問內存慢的短板

具體來講,線程讀/寫共享變量的步驟是這樣:

  1. 從主內存複製共享變量到本身的工做內存
  2. 在工做內存中對變量進行處理
  3. 處理完後,將變量值更新回主內存

假設如今主內存中有共享變量 X, 其初始值爲 0

線程1先訪問變量 X, 套用上面的步驟就是這樣:

  1. L1 和 L2 中都沒有發現變量 X,直到在主內存中找到
  2. 拷貝變量 X 到 L1 和 L2 中
  3. 在 L1 中將 X 的值修改成1,並逐層寫回到主內存中

此時,在線程 1 眼中,X 的值是這樣的:

接下來,線程 2 一樣按照上面的步驟訪問變量 X

  1. L1 中沒有發現變量 X
  2. L2 中發現了變量X
  3. 從L2中拷貝變量到L1中
  4. 在L1中將X 的值修改成2,並逐層寫回到主內存中

此時,線程 2 眼中,X 的值是這樣的:

結合剛剛的兩次操做,當線程1再訪問變量x,咱們看看有什麼問題:

此刻,若是線程 1 再次將 x=1回寫,就會覆蓋線程2 x=2 的結果,一樣的共享變量,線程拿到的結果卻不同(線程1眼中x=1;線程2眼中x=2),這就是共享變量內存不可見的問題。

怎麼補坑呢?今天的兩位主角閃亮登場,不過在說明 volatile關鍵字以前,咱們先來講說你最熟悉的 synchronized 關鍵字

synchronized

遇到線程不安全的問題,習慣性的會想到用 synchronized 關鍵字來解決問題,暫且先不論該辦法是否合理,咱們來看 synchronized 關鍵字是怎麼解決上面提到的共享變量內存可見性問題的

  • 【進入】synchronized 塊的內存語義是把在 synchronized 塊內使用的變量從線程的工做內存中清除,從主內存中讀取
  • 【退出】synchronized 塊的內存語義事把在 synchronized 塊內對共享變量的修改刷新到主內存中

二話不說,無情向下看 volatile

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】吧

靈魂追問

  1. 你瞭解線程的生命週期嗎?不一樣的狀態流轉是什麼樣的?
  2. 爲何線程有通知喚醒機制?

下一篇文章,咱們來講說【喚醒線程爲何建議用notifyAll而不建議用notify呢?】

我的博客:https://dayarch.top

加我微信好友, 進羣娛樂學習交流,備註「進羣」

歡迎持續關注公衆號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


相關文章
相關標籤/搜索