多方面解讀Java中的volatile關(guān)鍵字
介紹
volatile 是 Java 中的關(guān)鍵字,用于修飾變量。它的作用是強(qiáng)制對被修飾的變量的寫操作立即刷新到主存中,并強(qiáng)制對該變量的讀操作從主存中讀取最新的值,而不是使用緩存中的值。
作用
保證變量的可見性:
可見性指的是多個線程之間對共享變量的修改能否被及時地通知到其他線程,也就是說,當(dāng)一個線程修改了共享變量的值時,其他線程能夠立即看到這個變化。如果共享變量的可見性不能得到保證,就可能出現(xiàn)數(shù)據(jù)不一致的情況。
當(dāng)一個變量被 volatile 修飾時,任何對該變量的寫操作都會立即刷新到主存中,而任何對該變量的讀操作都會從主存中讀取最新的值。這樣可以保證多個線程之間對該變量的讀寫操作都是可見的。
禁止指令重排:
指令重排指的是編譯器或處理器為了提高程序運(yùn)行效率而對指令序列進(jìn)行重新排序的過程。在多線程環(huán)境下,指令重排可能會導(dǎo)致程序的執(zhí)行結(jié)果與預(yù)期結(jié)果不一致。因此,為了保證程序的正確性,需要使用 volatile 關(guān)鍵字或 synchronized 關(guān)鍵字來禁止指令重排。具體來說,當(dāng)一個變量被volatile修飾時,編譯器和處理器會插入一些內(nèi)存屏障,保證指令的執(zhí)行順序不會被打亂。
內(nèi)存屏障是一種特殊的CPU指令,它可以保證在執(zhí)行到內(nèi)存屏障之前的所有指令都已經(jīng)執(zhí)行完成,并且其結(jié)果已經(jīng)被寫入內(nèi)存中。在執(zhí)行內(nèi)存屏障之后的指令,必須等待前面的指令執(zhí)行完成后,才能開始執(zhí)行。這樣可以避免由于 CPU 的亂序執(zhí)行導(dǎo)致的指令重排和內(nèi)存操作的亂序問題。synchronized 和 volatile 在實(shí)現(xiàn)上都使用了內(nèi)存屏障來保證線程安全性。synchronized 使用了一種特殊的內(nèi)存屏障——“內(nèi)存鎖定”,它可以保證線程在獲取鎖之前,所有對共享變量的修改操作都已經(jīng)被刷新到主內(nèi)存中,同時在釋放鎖之后,所有對共享變量的修改操作都已經(jīng)對其他線程可見。而volatile使用了“寫屏障”和“讀屏障”,它們分別保證寫操作和讀操作在指令執(zhí)行時都不會受到指令重排的影響,從而保證了線程對共享變量的訪問操作的可見性和禁止指令重排。
編譯器和處理器為了提高程序的執(zhí)行效率,可能會對指令進(jìn)行重排。使用volatile關(guān)鍵字可以禁止這種優(yōu)化,保證指令的執(zhí)行順序不會被打亂。
不能保證原子性
原子性指的是一個操作是不可被中斷的,要么全部執(zhí)行,要么全部不執(zhí)行。在多線程環(huán)境下,如果某個操作不是原子性的,就可能會出現(xiàn)多個線程同時修改同一個變量的情況,從而導(dǎo)致數(shù)據(jù)不一致。
如果需要保證原子性,可以使用 synchronized 關(guān)鍵字或者juc.atomic包中的原子類。
可見性、有序性、原子性
可見性、有序性、原子性三個特性都是線程安全的必要條件,三個特性缺一不可。因此,保證其中任何一個特性都不能認(rèn)為是線程安全的全部,而只有同時保證三個特性才能實(shí)現(xiàn)真正的線程安全。
synchronized 關(guān)鍵字是一種保證線程安全的機(jī)制,它可以實(shí)現(xiàn)原子性和有序性,同時也可以保證可見性,因此使用 synchronized 可以保證線程安全,但是會影響程序的性能。synchronized 通過獨(dú)占鎖來保證同步代碼塊的原子性和有序性,同時在獲取和釋放鎖的過程中,會使用內(nèi)存屏障來保證可見性和有序性。
在Java 5之后,JDK 引入了 volatile 關(guān)鍵字,它可以保證可見性和有序性,但是無法保證原子性。volatile 關(guān)鍵字只能保證共享變量的讀寫操作是原子的,但是不能保證復(fù)合操作的原子性,如遞增操作。因此,在需要保證原子性的情況下,仍然需要使用 synchronized 關(guān)鍵字。
所以,使用 synchronized 可以保證線程安全,并且同時保證了可見性、有序性和原子性,而使用 volatile 只能保證可見性和有序性,無法保證原子性。
不會導(dǎo)致線程阻塞
線程阻塞指的是線程暫停執(zhí)行,等待某個條件得到滿足后再繼續(xù)執(zhí)行的一種狀態(tài)。線程阻塞可以分為兩種情況:
主動阻塞:主動阻塞是指線程在執(zhí)行過程中,調(diào)用了某個方法或者操作,而該方法或者操作需要等待某個條件的滿足才能繼續(xù)執(zhí)行。例如,線程調(diào)用了sleep方法,就會主動阻塞一段時間;線程調(diào)用了wait方法,就會主動阻塞,等待其他線程的喚醒。被動阻塞:被動阻塞是指線程在執(zhí)行過程中,由于某些原因(如I/O操作、鎖競爭等)而無法繼續(xù)執(zhí)行,進(jìn)入阻塞狀態(tài)。例如,線程等待某個資源的釋放,就會被動阻塞。
無論是主動阻塞還是被動阻塞,都會導(dǎo)致線程暫停執(zhí)行,直到某個條件得到滿足或者等待時間到期,才會重新被喚醒并繼續(xù)執(zhí)行。因此,在多線程編程中需要注意線程的阻塞狀態(tài),避免出現(xiàn)死鎖、饑餓等問題。
volatile 關(guān)鍵字不會阻塞線程,它只是保證對被修飾的變量的讀寫操作都從主存中進(jìn)行,而不是使用緩存中的值。
使用場景
1、多個線程之間共享一個變量,并且其中一個線程對該變量的修改需要立即對其他線程可見時。
/** * 將myVariable變量聲明為volatile類型。 * 在incrementmyVariable()方法中,我們對myVariable的值進(jìn)行了修改,而不是使用++運(yùn)算符。 * 這是因?yàn)?+運(yùn)算符不是一個原子操作,它實(shí)際上包含讀取變量值、增加變量值和寫回變量值這三個步驟, * 如果多個線程同時執(zhí)行這些步驟,可能會導(dǎo)致值的不一致。 * 因此,使用了一個簡單的加法操作來對myVariable進(jìn)行修改,這樣就可以確保線程安全。 * @author Administrator */ public class MyVolatile1 { private volatile int myVariable = 0; public void incrementMyVariable() { myVariable = myVariable + 1; } public int getMyVariable() { return myVariable; } }
2、多個線程之間共享一個變量,并且該變量的值可能會被多個線程同時修改時。
/** * 將myVariable變量聲明為volatile類型。由于多個線程可能同時修改該變量的值, * 需要使用synchronized關(guān)鍵字來保護(hù)它。 * incrementMyVariable()、decrementMyVariable()、getMyVariable()方法中, * 都使用了synchronized關(guān)鍵字來確保對共享變量的訪問是原子的。這樣就可以確保線程安全。 * * * * 為什么myVariable = myVariable + 1;是線程安全;myVariable ++不是? * myVariable++操作實(shí)際上包含了三個步驟: * 1、讀取myVariable的值、 * 2、將其增加1、 * 3、將結(jié)果寫回到myVariable。 * 如果有多個線程同時執(zhí)行myVariable++操作,就會出現(xiàn)競態(tài)條件,導(dǎo)致myVariable的值不確定。 * * 例如,假設(shè)myVariable的初始值為0,有兩個線程同時執(zhí)行myVariable++操作,它們可能會執(zhí)行以下步驟: * * 線程1讀取myVariable的值為0。 * 線程2讀取myVariable的值為0。 * 線程1將myVariable的值增加1,結(jié)果為1。 * 線程2將myVariable的值增加1,結(jié)果也為1。 * 線程1將結(jié)果1寫回到myVariable。 * 線程2將結(jié)果1寫回到myVariable。 * 最終,myVariable的值為1,而不是預(yù)期的2。這就是競態(tài)條件導(dǎo)致的結(jié)果。 * * 相比之下,myVariable = myVariable + 1; * 操作只包含兩個步驟:讀取myVariable的值和將其增加1。由于這兩個步驟是在一個原子操作中完成的,所以不會出現(xiàn)競態(tài)條件,從而保證了線程安全。 * * 因此,當(dāng)我們需要對共享變量進(jìn)行增加操作時,建議使用myVariable = myVariable + 1;這種形式,而不是myVariable++操作,以確保線程安全。 * @author Administrator */ public class MyVolatile2 { private volatile int myVariable = 0; public synchronized void incrementMyVariable() { myVariable = myVariable + 1; } public synchronized void decrementMyVariable() { myVariable = myVariable - 1; } public synchronized int getMyVariable() { return myVariable; } }
實(shí)現(xiàn)原理
使用內(nèi)存屏障:當(dāng)一個變量被 volatile 修飾時,編譯器和處理器會插入一些內(nèi)存屏障,保證指令的執(zhí)行順序不會被打亂。
使用緩存一致性協(xié)議:當(dāng)一個線程對一個 volatile 變量進(jìn)行寫操作時,處理器會發(fā)送一個信號通知其他處理器該變量的值已經(jīng)被修改,其他處理器會強(qiáng)制將該變量的緩存行失效,從而保證其他線程讀取該變量時能夠讀取到最新的值。
happens-before
happens-before,用于描述多線程程序中的事件順序關(guān)系。如果一個事件 A happens-before 另一個事件B,那么A對共享變量的寫入操作對B的讀取操作是可見的,也就是說,B 可以看到 A 寫入的值。happens-before 關(guān)系可以通過以下方式建立: 1、程序順序規(guī)則:在一個線程中,按照程序順序,前面的操作 happens-before 于后面的操作。 2、volatile變量規(guī)則:對一個 volatile 變量的寫操作 happens-before 于后續(xù)對該變量的讀操作。 3、鎖定規(guī)則:一個解鎖操作 happens-before 于后續(xù)的加鎖操作。 4、傳遞性:如果A happens-before B,B happens-before C,那么A happens-before C。
局限性
不能保證原子性:volatile關(guān)鍵字只能保證可見性和禁止指令重排,不能保證原子性。如果需要保證原子性,可以使用synchronized 關(guān)鍵字或者 juc.atomic包中的原子類。不能替代鎖:volatile 關(guān)鍵字只能保證可見性,不能保證同步性。如果需要保證同步性,仍然需要使用 synchronized 關(guān)鍵字或者其他鎖機(jī)制。 對于復(fù)合操作的保證:當(dāng)多個變量之間存在依賴關(guān)系時,使用 volatile 關(guān)鍵字不能保證操作的原子性和一致性。此時仍然需要使用 synchronized 關(guān)鍵字或者其他鎖機(jī)制來保證操作的一致性。
和 synchronized 關(guān)鍵字比較
volatile關(guān)鍵字只能修飾變量,而synchronized關(guān)鍵字可以修飾方法或代碼塊。 volatile關(guān)鍵字不能保證原子性,而synchronized關(guān)鍵字可以保證原子性。 volatile關(guān)鍵字不會阻塞線程,而synchronized關(guān)鍵字可能會導(dǎo)致線程阻塞。 volatile關(guān)鍵字只能保證可見性和禁止指令重排,而synchronized關(guān)鍵字可以保證同步性和原子性。 volatile關(guān)鍵字用于保證可見性和禁止指令重排,但是不能保證原子性,不能替代鎖機(jī)制。 synchronized關(guān)鍵字用于保證同步性和原子性,但是不能保證可見性。synchronized關(guān)鍵字通過獲取鎖來保證同一時刻只有一個線程可以執(zhí)行臨界區(qū)代碼,從而保證線程安全。
和 Atomic 類比較
volatile 關(guān)鍵字只能保證可見性和禁止指令重排,不能保證原子性。 Atomic 類可以保證可見性和原子性,但是不能替代鎖機(jī)制。Atomic 類通過CAS操作實(shí)現(xiàn)原子性,當(dāng)多個線程同時對一個變量進(jìn)行修改時,只有一個線程能夠成功執(zhí)行 CAS 操作,從而保證操作的原子性。
和 final 關(guān)鍵字比較
volatile關(guān)鍵字用于保證可見性和禁止指令重排,但是不能保證原子性,不能替代鎖機(jī)制。 final關(guān)鍵字用于定義常量,一旦定義就不能再修改它的值。final關(guān)鍵字可以保證線程安全,因?yàn)橐粋€final變量的值一旦被初始化之后就不會再被修改,所以不會存在多線程之間的競爭問題。
和 ThreadLocal 關(guān)鍵字比較
volatile 關(guān)鍵字用于保證可見性,即多個線程之間能夠正確地訪問共享變量,避免出現(xiàn)數(shù)據(jù)不一致的情況。
ThreadLocal 用于實(shí)現(xiàn)線程本地變量,即每個線程都有自己獨(dú)立的變量副本,線程之間不會相互干擾。ThreadLocal 主要解決的是多線程中數(shù)據(jù)的隔離問題,可以讓每個線程都擁有自己獨(dú)立的變量副本,從而避免出現(xiàn)數(shù)據(jù)不一致的情況。
失效場景
1、如果變量的值只會被單個線程修改,而其他線程只會讀取這個值,那么就算不使用volatile關(guān)鍵字,這個程序也是線程安全的。 在這種情況下,即使不使用volatile關(guān)鍵字,多個線程之間也不會產(chǎn)生競爭條件,因?yàn)槊總€線程都是獨(dú)立地讀取和修改變量的值。因此,volatile關(guān)鍵字在這種情況下并不會產(chǎn)生任何影響。
// 場景1:如果變量的值只會被單個線程修改,那么即使不使用volatile關(guān)鍵字,程序也是線程安全的。 public static void testScenario1() throws InterruptedException { /** * 即使使用 newCachedThreadPool 創(chuàng)建的線程池,每個任務(wù)也只會被單個線程執(zhí)行。 * 因?yàn)榫€程池會維護(hù)一定數(shù)量的線程,當(dāng)有新任務(wù)時會從線程池中取出一個空閑的線程執(zhí)行, * 當(dāng)任務(wù)執(zhí)行完畢后,該線程就會重新歸還到線程池中等待下一個任務(wù)的到來。 * * 在這個例子中,雖然沒有顯式地指定線程數(shù),但是線程池內(nèi)部會根據(jù)需要自動創(chuàng)建和維護(hù)線程。 * 在執(zhí)行任務(wù)時,線程池會選取一個空閑的線程去執(zhí)行任務(wù),而其他線程都會被阻塞等待。 * 因此,雖然任務(wù)可能被多個線程執(zhí)行,但每個線程只會執(zhí)行部分任務(wù),任務(wù)的修改操作也只會被單個線程執(zhí)行,因此這個程序是線程安全的。 */ //ExecutorService executorService = Executors.newCachedThreadPool(); ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(() -> { for (int i = 0; i < 10000; i++) { count++; } }); executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.SECONDS); System.out.println("Scenario1: count=" + count); }
2、多個線程對變量進(jìn)行復(fù)合操作,例如 i++ 操作,這種操作不是原子性操作,volatile關(guān)鍵字無法保證操作的一致性。
在這種情況下,如果多個線程同時對變量進(jìn)行復(fù)合操作,例如i++操作,會導(dǎo)致競爭條件的產(chǎn)生,從而導(dǎo)致線程安全問題。雖然volatile關(guān)鍵字可以保證變量的可見性,但是它無法保證操作的原子性,因此使用volatile關(guān)鍵字并不能解決這種情況下的線程安全問題。
如果需要保證操作的原子性,可以使用synchronized關(guān)鍵字或者juc.atomic包中提供的原子類。
// 場景2:多個線程對變量進(jìn)行復(fù)合操作,volatile關(guān)鍵字無法保證操作的一致性。 public static void testScenario2() throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executorService.execute(() -> { for (int j = 0; j < 1000; j++) { count++; } }); } executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.SECONDS); System.out.println("Scenario2: count=" + count); }
3、多個線程之間對變量的操作存在依賴關(guān)系,但是依賴關(guān)系沒有被正確處理,例如線程A對變量 i 進(jìn)行操作后,線程 B 讀取了i的值,但是沒有正確處理線程 A 對 i 的操作,導(dǎo)致線程 B 讀取到了錯誤的值。
在這種情況下,volatile關(guān)鍵字可以保證變量的可見性,但是它無法保證多個線程之間的操作順序。如果多個線程之間的操作存在依賴關(guān)系,那么需要使用同步機(jī)制,例如synchronized關(guān)鍵字或者 juc 包中提供的同步工具,來保證操作的順序和一致性。 B 讀取了i的值,但是沒有正確處理線程 A 對 i 的操作,導(dǎo)致線程 B 讀取到了錯誤的值。
// 場景3:多個線程之間對變量的操作存在依賴關(guān)系,但是依賴關(guān)系沒有被正確處理,使用volatile關(guān)鍵字也無法保證正確性。 public static void testScenario3() throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.execute(() -> { for (int i = 0; i < 1000; i++) { count++; } }); executorService.execute(() -> { for (int i = 0; i < 1000; i++) { if (count % 2 == 0) { count++; } } }); executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.SECONDS); System.out.println("Scenario3: count=" + count); }
測試
public class VolatileTest { private static volatile int count = 0; public static void main(String[] args) throws InterruptedException { // 測試場景1:單個線程修改變量值 testScenario1(); // 測試場景2:多個線程對變量進(jìn)行復(fù)合操作 testScenario2(); // 測試場景3:多個線程之間存在依賴關(guān)系 testScenario3(); } }
結(jié)果
到此這篇關(guān)于多方面解讀Java中的volatile關(guān)鍵字的文章就介紹到這了,更多相關(guān)Java中的volatile關(guān)鍵字內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java描述數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí)之鏈表的增刪改查詳解
這篇文章主要給大家介紹了關(guān)于Java描述數(shù)據(jù)結(jié)構(gòu)學(xué)習(xí)之鏈表的增刪改查的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-05-05Java8利用Stream實(shí)現(xiàn)列表去重的方法詳解
這篇文章主要為大家介紹了Java利用Stream實(shí)現(xiàn)列表去重的幾種方法詳解,文中的示例代碼講解詳細(xì),需要的小伙伴可以參考一下2022-04-04java在linux本地執(zhí)行shell命令的實(shí)現(xiàn)方法
本文主要介紹了java在linux本地執(zhí)行shell命令的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02Spring Security中successHandler和failureHandler使用方式
這篇文章主要介紹了Spring Security中successHandler和failureHandler使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08jdbc連SQL?server顯示1433端口連接失敗圖文解決方法
這篇文章主要給大家介紹了關(guān)于jdbc連SQL?server顯示1433端口連接失敗的圖文解決方法,文中通過圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考借鑒價值,需要的朋友可以參考下2024-06-06Struts2學(xué)習(xí)筆記(6)-簡單的數(shù)據(jù)校驗(yàn)
這篇文章主要介紹Struts2中的數(shù)據(jù)校驗(yàn),通過一個簡單的例子來說明,希望能給大家做一個參考。2016-06-06