淺談 synchronized 和 volatitle 實現線程安全的策略

什麼是線程不安全

對線程安全的理解就是多個線程同時操作一個共享變量時會產生意料之外的情況,這種情況就是線程不安全。注意:只有寫操作纔可能出現線程不安全,對共享變量只進行讀操作線程是絕對安全的。

具體線程不安全的例子有一個很經典的就是兩個線程都對一個共享變量 x=0 執行 100 次自增操作,但是 x 的結果並非 200

因此線程不安全的條件是:多線程 + 共享變量 + 寫操作

Java 的內存模型

你可能會好奇線程是如何獲取共享變量的?

Java 線程之間的通信由 Java 內存模型(簡稱 JMM)控制,從抽象的角度來說,JMM 定義了線程和主內存之間的抽象關係。JMM 的抽象示意圖如圖所示:

從圖中可以看到:

以上只是 Java 內存模型的抽象圖,實際上線程的工作模型是這樣的,棧內存即是兩個緩衝區

接下來看一個線程不安全的例子:

假設線程 A、B 操作同一個共享變量 X,初始兩級 Cache 都爲空

  1. 線程 A 想要讀取 X 的值,由於兩級 Cache 都沒有命中,因此加載堆內存中的 X=0,並緩存到兩個 Cache 中

  2. 線程 A 修改 X 的值爲 1,爲爲兩個 Cache 刷新 X,再刷新到堆內存

  3. 線程 B 想要獲取 X 的值,一級緩存沒有獲取到,二級緩存命中,讀取到 X=1

  4. 線程 B 想要修改 X 的值爲 2,先刷新自,己的一級緩存爲 2,再刷新二級緩存和堆內存中的 X 爲 2。目前爲止一切正常,接下來重點來了

  5. 線程 A 想要讀取 X 的值,一級緩存命中此時 X=1,但是堆內存中的 X=2。可以看到線程 B 寫入的共享變量對 X 不可見,出現了線程不安全的情況。

由於 Java 內存機制就是這樣設計的,因此多個線程操作同一個變量會產生不安全的問題,volatitle 關鍵字這是被設計出來解決這一問題的,它只能用於單個變量。

volatile 解決共享變量線程不安全的策略

還是接着上面這個例子,這樣定義 X

volatle int X=0

volatile 的內存語義是:

當一個線程對 volatile 修飾的變量進行寫操作時,JMM 會立即將該線程對應的棧內存中的副本的值刷新到堆內存中;當一個線程對 volatile 修飾的變量進行讀時,JMM 會清空此變量的一二級緩存,直接從堆內存中讀取共享變量的值。

volatile 可以當作一個輕量級的鎖來使用,但 volatile 僅僅只能保證共享變量內存的可見性,不能保證操作共享變量的原子性,而鎖(如 synchronized)能保證整段鎖範圍內的代碼具有原子性。

synchronized 與鎖

首先要明確的是 synchronized 不是鎖,鎖都是基於對象的 (Object 的子類),Java 中的每一個對象都可以作爲一個鎖。

synchronized 是 Java 的一個關鍵字,保證臨界區內的代碼同一時刻只能有一個線程執行。

線程的執行代碼在進入 synchronized 代碼塊前會自動獲取內部鎖,這時候其他線程訪問該同步代碼塊時會被阻塞掛起。拿到內部鎖的線程會在正常退出同步代碼塊或者拋出異常後或者在同步塊內調用了該內置鎖資源的 wait 系列方法時釋放該內置鎖。內置鎖是排它鎖,也就是當一個線程獲取這個鎖後,其他線程必須等待該線程釋放鎖後才能獲取該鎖。

sysnchronized 的內部鎖可以是:

wait 和 notify 方法只能用於 synchronized 同步代碼塊內

synchronized 的內存語義

與 volatile 不同

進入 synchronized 塊的內存語義是把再 synchronized 塊內使用到的所有共享變量從棧內存中清空,這樣就只能從堆內存只讀取,保證了內存可見性。退出 synchronized 塊的內存語義是把 synchronized 塊內對共享變量的修改刷新到堆內存。

仔細想想,這其實也是一個加鎖和解鎖的過程,保證共享變量修改的可見性。

總結

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wLPhJREh6n3qKPndOfowxQ