您好,登錄后才能下訂單哦!
本篇內容介紹了“什么是volatile機制”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
我們都知道synchronized關鍵字的特性:原子性、可見性、有序性、可重入性,雖然,JDK在不斷的嘗試優化這個內置鎖,一文中有提到:無鎖 -> 偏向鎖 -> 輕量鎖 -> 重量鎖 一共四種狀態,但是,在高并發的情況下且大量沖突出現的時候,最終都還是會膨脹到重量鎖。
那是因為,synchronized是同步代碼塊,通過monitor監視器,對整個代碼塊(方法是通過判斷 ACC_SYNCHRONZED 標志位對整個方法)進行了整體原子性操作。而 volatile 對單一操作是原子性的,非單一操作則是非原子性的。
Java語言里的volatile關鍵字是用來修飾變量的,方式如下入所示。表示:該變量需要直接存儲到主內存中。
public class SharedClass { public volatile int counter = 0; }
被volatile關鍵字修飾的 int counter 變量會直接存儲到主內存中。并且所有關于該變量的讀操作,都會直接從主內存中讀取,而不是直接從CPU緩存。(關于主內存和CPU緩存的區別,如果不理解也不用擔心,下面會詳細介紹)
這么做解決什么問題呢?主要是兩個問題:
多線程見可見性的問題,
CPU指令重排序的問題
注:為了描述方便,我們接下來會把 volatile 修飾的變量簡稱為“volatile 變量”,把沒有用 volatile 修飾的變量建成為“non-volatile”變量。
變量可見性問題(Variable Visibility Problem) : volatile可以保證變量變化在多線程間的可見性。
一個多線程應用中,出于計算性能的考慮,每個線程默認是從主內存將該變量拷貝到線程所在CPU的緩存中,然后進行讀寫操作的。現在電腦基本都是多核CPU,不同的線程可能運行的不同的核上,而每個核都會有自己的緩存空間。如下圖所示(圖中的 CPU 1,CPU 2 大家可以直接理解成兩個核):
這里存在一個問題,JVM既不會保證什么時候把 CPU 緩存里的數據寫到主內存,也不會保證什么時候從主內存讀數據到 CPU 緩存。也就是說,不同 CPU 上的線程,對同一個變量可能讀取到的值是不一致的,這也就是我們通常說的:線程間的不可見問題。
比如下圖,Thread 1 修改的 counter = 7 只在 CPU 1 的緩存內可見,Thread 2 在自己所在的 CPU 2 緩存上讀取 counter 變量時,得到的變量 counter 的值依然是 0。
而volatile出現的用意之一,就是要解決線程間不可見性,通過 volatile 修飾的變量,都會變得線程間可見。
其解決方式就是文章開頭提到的:
通過 volatile 修飾的變量,所有關于該變量的讀操作,都會直接從主內存中讀取,而不是 CPU 自己的緩存。而所有該變量的寫操都會寫到主內存上。
因為主內存是所有 CPU 共享的,理所當然即使是不同 CPU 上的線程也能看到其他線程對該變量的修改了。volatile不僅僅只保證 volatile變量的可見性,volatile 在可見性上所做的工作,實際上比保證 volatile 變量的可見性更多:
當 Thread A 修改了某個被 volatile 變量 V,另一個 Thread B 立馬去讀該變量 V。一旦 Thread B 讀取了變量 V 后,不僅僅是變量 V 對 Thread B 可見, 所有在 Thread A 修改變量 V 之前 Thread A 可見的變量,都將對 Thread B 可見。
當 Thread A 讀取一個 volatile 變量 V 時,所有對于 Thread A 可見的其他變量也都會從主內存中被讀取。
任意一個線程修改了 volatile 修飾的變量,其他線程可以馬上識別到最新值。實現可見性的原理如下。
步驟 1:修改本地內存,強制刷回主內存。
步驟 2:強制讓其他線程的工作內存失效過期。(此部分更多的屬于MESI協議)
單個volatile變量的讀/寫(比如 vl=l)具有原子性,復合操作(比如 i++)不具有原子性,Demo 代碼如下:
public class VolatileFeaturesA { private volatile long vol = 0L; /** * 單個讀具有原子性 * @date:2020 年 7 月 14 日 下午 5:02:38 */ public long get() { return vol; } /** * 單個寫具有原子性 * @date:2020 年 7 月 14 日 下午 5:01:49 */ public void set(long l) { vol = l; } /** * 復合(多個)讀和寫不具有原子性 * @date:2020 年 7 月 14 日 下午 5:02:24 */ public void getAndAdd() { vol++; } }
同一時刻只允許一個線程操作 volatile 變量,volatile 修飾的變量在不加鎖的場景下也能實現有鎖的效果,類似于互斥鎖。上面的 VolatileFeaturesA.java 和下面的 VolatileFeaturesB.java 兩個類實現的功能是一樣的(除了 getAndAdd 方法)。
public class VolatileFeaturesB { private volatile long vol = 0L; /** * 普通寫操作 * @date:2020 年 7 月 14 日 下午 8:18:34 * @param l */ public synchronized void set(long l) { vol = l; } /** * 加 1 操作 * @author songjinzhou * @date:2020 年 7 月 14 日 下午 8:28:25 */ public void getAndAdd() { long temp = get(); temp += 1L; set(temp); } /** * 普通讀操作 * @date:2020 年 7 月 14 日 下午 8:33:00 * @return */ public synchronized long get() { return vol; } }
JVM 是使用內存屏障來禁止指令重排,從而達到部分有序性效果,看看下面的 Demo 代碼分析自然明白為什么只是部分有序:
//a、b 是普通變量,flag 是 volatile 變量 int a = 1; //代碼 1 int b = 2; //代碼 2 volatile boolean flag = true; //代碼 3 int a = 3; //代碼 4 int b = 4; //代碼 5
因為 flag 變量是使用 volatile 修飾,則在進行指令重排序時,不會把代碼 3 放到代碼 1 和代碼 2 前面,也不會把代碼 3 放到代碼 4 或者代碼 5 后面。 但是指令重排時代碼 1 和代碼 2 順序、代碼 4 和代碼 5 的順序不在禁止重排范圍內,比如:代碼 2 可能會被移到代碼 1 之前。
LoadLoadBarriers
指令示例:LoadA —> Loadload —> LoadB
此屏障可以保證 LoadB 和后續讀指令都可以讀到 LoadA 指令加載的數據,即讀操作 LoadA 肯定比 LoadB 先執行。
StoreStoreBarriers
指令示例:StoreA —> StoreStore —> StoreB
此屏障可以保證 StoreB 和后續寫指令可以操作 StoreA 指令執行后的數據,即寫操作 StoreA 肯定比 StoreB 先執行。
LoadStoreBarriers
指令示例: LoadA —> LoadStore —> StoreB
此屏障可以保證 StoreB 和后續寫指令可以讀到 LoadA 指令加載的數據,即讀操作 LoadA 肯定比寫操作 StoreB 先執行。
StoreLoadBarriers
指令示例:StoreA —> StoreLoad —> LoadB
此屏障可以保證 LoadB 和后續讀指令都可以讀到 StoreA 指令執行后的數據,即寫操作 StoreA 肯定比讀操作 LoadB 先執行。
如果屬性使用了 volatile 修飾,在編譯的時候會在該屬性的前或后插入上面介紹的 4 類內存屏障來禁止指令重排,比如:
在 volatile 寫操作的前面插入 StoreStoreBarriers 保證volatile寫操作之前的普通讀寫操作執行完畢后再執行 volatile 寫操作。
在 volatile 寫操作的后面插入 StoreLoadBarriers 保證 volatile 寫操作后的數據刷新到主內存,保證之后的 volatile 讀寫操作能使用最新數據(主內存)。
在 volatile 讀操作的后面插入 LoadLoadBarriers 和 LoadStoreBarriers 保證 volatile 讀寫操作之后的普通讀寫操作先把線程本地的變量置為無效,再把主內存的共享變量更新到本地內存,之后都使用本地內存變量。
volatile 讀操作內存屏障:
volatile 寫操作內存屏障:
狀態標志,比如布爾類型狀態標志,作為完成某個重要事件的標識,此標識不能依賴其他任何變量,Demo 代碼如下:
public class Flag { //任務是否完成標志,true:已完成,false:未完成 volatile boolean finishFlag; public void finish() { finishFlag = true; } public void doTask() { while (!finishFlag) { //keep do task } }
一次性安全發布,比如:著名的 double-checked-locking,demo 代碼上面已貼出。 開銷較低的讀,比如:計算器,Demo 代碼如下。
/** * 計數器 */ public class Counter { private volatile int value; //讀操作無需加鎖,減少同步開銷提交性能,使用 volatile 修飾保證讀操作的可見性,每次都可以讀到最新值 public int getValue() { return value; } //寫操作使用 synchronized 加鎖,保證原子性 public synchronized int increment() { return value++; } }
“什么是volatile機制”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。