國外家譜網(wǎng)站的建設關(guān)鍵詞排名怎么做上首頁
文章目錄
- 優(yōu)秀引用
- 1、概述
- 2、可見性保證
- 2.1、什么是可見性
- 2.2、例子舉證
- 2.3、結(jié)果解析
- 3、有序性保證
- 3.1、什么是有序性
- 3.2、什么是重排序
- 3.3、例子舉證
- 4、無法保證原子性
- 4.1、什么是原子性
- 4.2、例子舉證
- 5、內(nèi)存屏障
- 5.1、什么是內(nèi)存屏障
- 5.2、不同內(nèi)存屏障的作用
- 6、volatile和synchronized的區(qū)別
- 7、使用場景
- 7.1、多線程共享變量
- 7.2、雙重檢查鎖定
- 7.3、狀態(tài)標志
優(yōu)秀引用
尚硅谷JUC并發(fā)編程(對標阿里P6-P7)之volatile
Java中不可或缺的關(guān)鍵字「volatile」
全面理解Java的內(nèi)存模型(JMM)
1、概述
在多線程編程中,確保線程安全和正確的執(zhí)行順序是非常重要的。由于多線程環(huán)境下,不同線程之間共享內(nèi)存資源,因此對這些資源的訪問必須進行同步以避免出現(xiàn)競態(tài)條件等問題。Java中提供了多種方式來實現(xiàn)同步,其中 volatile
是一種非常輕量級的同步機制。
volatile
直譯過來是“不穩(wěn)定的”,意味著被其修飾的屬性可能隨時發(fā)生變化。該關(guān)鍵字為Java提供了一個輕量級的同步機制:保證被volatile修飾的共享變量對所有線程總是可見的,也就是當一個線程修改了一個被 volatile
修飾共享變量的值,新值總是可以被其他線程立即得知。相較于我們熟知的重量級鎖 synchronized
,volatile
更輕量級,因為它不會引起上下文切換和線程調(diào)度。
volatile
關(guān)鍵字的特性主要有以下幾點:
- 保證可見性:當一個變量被聲明為
volatile
時,所有線程都可以看到它的最新值,即每次讀取都是從主內(nèi)存中獲取最新值,而不是從線程的本地緩存中獲取舊值; - 保證有序性:
volatile
關(guān)鍵字可以禁止指令重排序。編譯器和CPU為了提高代碼執(zhí)行效率,可能會對指令進行重排序,這可能會導致線程安全問題。但是,當一個變量被聲明為volatile
時,編譯器和CPU會禁止對它進行指令重排序,保證指令執(zhí)行的正確順序; - 無法保證原子性:
volatile
關(guān)鍵字并不能保證操作過程中的有序性,如果需要保證一系列操作的原子性,仍然需要借助鎖機制進行限制。
2、可見性保證
2.1、什么是可見性
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他縣城能夠立即看到修改的值。
2.2、例子舉證
我們通過一個循環(huán)的例子進行舉證,大致是使用一個變量標識一個 while
循環(huán),通過新線程修改這個標識,進而查看循環(huán)是否會結(jié)束。接下來將會對未加上和加上 volatile
進行舉例查看結(jié)果。
- 未加
volatile
的普通flag。
public class VolatileSeeTest {static boolean flag = true;public static void main(String[] args) throws InterruptedException {new Thread(() -> {int i = 1;while (flag) {if (i == 1) {System.out.println("掉坑里了……");i = 0;}}System.out.println("我出來啦!.(flag此時為" + flag);}, "t1").start();// 等待確保上面t1線程已經(jīng)執(zhí)行Thread.sleep(1000L);flag = false;System.out.println("好小子,速速跳出坑來.(flag此時為" + flag);}
}================================================此時結(jié)果打印:
掉坑里了……
好小子,速速跳出坑來.(flag此時為false)
(程序還未結(jié)束,代表t1線程還在死循環(huán)中)================================================
- 加上
volatile
的flag。
public class VolatileSeeTest {static volatile boolean flag = true;public static void main(String[] args) throws InterruptedException {new Thread(() -> {int i = 1;while (flag) {if (i == 1) {System.out.println("掉坑里了……");i = 0;}}System.out.println("我出來啦!.(flag此時為" + flag);}, "t1").start();// 等待確保上面t1線程已經(jīng)執(zhí)行Thread.sleep(1000L);flag = false;System.out.println("好小子,速速跳出坑來.(flag此時為" + flag);}
}================================================掉坑里了……
好小子,速速跳出坑來.(flag此時為false)
我出來啦!.(flag此時為false)Process finished with exit code 0(程序已經(jīng)結(jié)束)================================================
2.3、結(jié)果解析
針對于第一種沒有 volatile
關(guān)鍵字修飾的情況,很明顯 主線程 對 flag 變量的修改對 t1 線程并不可見,導致 t1 線程中的循環(huán)并未跳出。這是因為 主線程 和 t1 線程中分別都對 flag 變量進行了拷貝,備份到了各自中的本地緩存(也叫做工作內(nèi)存或本地內(nèi)存)中,當兩個線程讀取 flag 變量時都是從本地緩存中讀取,主線程 中對 flag 變量進行的操作對 t1 線程并不可見,導致每次 t1 線程讀取 flag 變量時都是初始保存的 false。
根本原因是因為沒有
volatile
關(guān)鍵字修飾的變量并沒有及時的從主存中讀取最新值和往主存中寫入自己修改的值,如果其他線程要訪問這個變量,它們可能會直接從自己的本地緩存中讀取這個變量的值,而不是從主內(nèi)存中讀取,導致在多線程環(huán)境下不同線程之間的數(shù)據(jù)出現(xiàn)不一致情況。
針對于第二種添加了 volatile
關(guān)鍵字修飾的情況,通過結(jié)果我們可以看出 t1 線程成功跳出了循環(huán)最終程序結(jié)束,證明了 volatile
關(guān)鍵字是可以保證可見性的。這是因為被 volatile
修飾的 flag 變量被修改后,JMM 會把該線程本地緩存中的這個 flag 變量立即強制刷新到主內(nèi)存中去,導致 t1 線程中的 flag 變量緩存無效,也就是說其他線程使用 volatile
修飾的 flag 變量時,都是從主內(nèi)存刷新的最新數(shù)據(jù)。
3、有序性保證
3.1、什么是有序性
所謂的有序性,顧名思義就是程序執(zhí)行的順序按照指定的順序先后執(zhí)行。
3.2、什么是重排序
現(xiàn)代的計算機為了提高性能,在程序運行過程中常常會對指令進行重排序,這就涉及到了為此誕生的 流水線技術(shù)。
所謂的 流水線技術(shù),就是指一個CPU指令的執(zhí)行過程可以分為4個階段:取指、譯碼、執(zhí)行、寫回。它的原理是在不影響程序運行結(jié)果的情況下,指令1還沒有執(zhí)行完,就可以開始執(zhí)行指令2,而不用等到指令1執(zhí)行結(jié)束之后再執(zhí)行指令2,這樣就大大提高了效率。
但是在多線程的情況下,指令重排可能會影響本地緩存和主存之間交互的方式,造成亂序問題最終導致數(shù)據(jù)錯亂。指令重排一般可以分為下面三種類型:
- 編譯器優(yōu)化重排。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
- 指令并行重排?,F(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性(即后一個執(zhí)行的語句無需依賴前面執(zhí)行的語句的結(jié)果),處理器可以改變語句對應的機器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)重排。由于處理器使用緩存和讀寫緩存沖區(qū),這使得加載(load)和存儲(store)操作看上去可能是在亂序執(zhí)行,因為三級緩存的存在,導致內(nèi)存與緩存的數(shù)據(jù)同步存在時間差。
3.3、例子舉證
了解過單例模式的小伙伴可能都了解過,雙重校驗鎖有一個volatile版本的:
public class Singleton {// 私有構(gòu)造方法private Singleton() {}// 使用volatile禁止單例對象創(chuàng)建時的重排序private static volatile Singleton instance;// 對外提供靜態(tài)方法獲取該對象public static Singleton getInstance() {// 第一次判斷,如果instance不為null,不進入搶鎖階段,直接返回實際if(instance == null) {synchronized (Singleton.class) {// 搶到鎖之后再次判斷是否為空if(instance == null) {instance = new Singleton();}}}return instance;}
}
有小伙伴可能會問到不是已經(jīng)上了鎖并且都進行判斷了嘛,怎么還會有并發(fā)問題,還得加上 volatile
關(guān)鍵字解決的。這就得扯到在多線程環(huán)境下對 instant 對象實例化時計算機對其的指令重排了:
當一個線程執(zhí)行到第一次判空時,由于 instant 還沒有被初始化,因此會進入同步塊中進行初始化操作。但是,在初始化過程中,由于指令重排序的影響, instant 可能會被先分配空間并賦值,然后再進行構(gòu)造函數(shù)的初始化操作。此時,如果有另外一個線程進入了第一次判空,并且發(fā)現(xiàn) instant 不為 null
,就會直接返回一個尚未完成初始化的實例,從而導致并發(fā)問題。
4、無法保證原子性
4.1、什么是原子性
原子性是指一個操作或者一系列操作要么全部執(zhí)行成功,要么全部不執(zhí)行,不會出現(xiàn)部分執(zhí)行的情況。
4.2、例子舉證
常見的非原子性操作便是自增操作,因為自增操作在指令層面可以分為三步:
- i 被從局部變量表(內(nèi)存)取出;
- 壓入操作棧(寄存器),操作棧中自增;
- 使用棧頂值更新局部變量表(寄存器更新寫入內(nèi)存)。
我們對 volatile 修飾的變量進行自增操作,通過查看結(jié)果來驗證這一特性:
public class VolatileAtomicTest {public static volatile int val;public static void add() {for (int i = 0; i < 1000; i++) {val++;}}public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(Test1::add);Thread t2 = new Thread(Test1::add);t1.start();t2.start();// 等待線程運算結(jié)束t1.join();t2.join();// 打印結(jié)果System.out.println(val);}
}
按照正常情況,最終輸出的應該是2000,但是我們運行起來會發(fā)現(xiàn)結(jié)果并不如意,絕大多數(shù)情況下都會低于2000,從而驗證了 volatile 并不能保證原子性。這是因為多線程環(huán)境下,可能 線程t1 正在進行 第i次
的 取值-運算-賦值 操作時,另外一個 線程t2 已經(jīng)完成了操作并提交到了主存中,主存就會通知 線程t1 本地緩存中的數(shù)據(jù)已經(jīng)過時,從而丟棄手中正在進行的對數(shù)據(jù)的操作,去獲取最新的數(shù)據(jù),導致 線程t1 要開始 第i+1次
運算從而浪費了 第i次
的運算機會,導致最終的結(jié)果沒有達到我們預想的2000。
原子性的保證可以通過 synchronized、Lock、Atomic
5、內(nèi)存屏障
5.1、什么是內(nèi)存屏障
內(nèi)存屏障,也稱內(nèi)存柵欄,是一類同步屏障指令,是CPU或編譯器在對內(nèi)存隨機訪問的操作中的一個同步點,是的詞典之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行詞典之后的操作,避免代碼的重排序。
內(nèi)存屏障其實就是一種JVM指令,Java內(nèi)存模型的重排規(guī)則會要求Java編譯器在生成JVM指令時插入特定的內(nèi)存屏障指令,通過這些內(nèi)存屏障指令,volatile實現(xiàn)了Java內(nèi)存模型中的可見性和有序性。
通過對有 volatile 關(guān)鍵字修飾的變量進行操作的代碼進行反編譯我們會發(fā)現(xiàn),在 volatile 范圍內(nèi)多了個lock前綴指令,這里簡單介紹一下這一指令的作用。
當一個變量被volatile修飾后,它在讀寫時會使用一種特殊的機器指令(lock前綴指令),這個指令可以保證多個線程在讀寫這個變量時不會出現(xiàn)問題:
- 寫volatile變量時,會先把變量的值寫入到CPU緩存中,然后再把緩存中的數(shù)據(jù)寫入到主內(nèi)存中,這樣其他線程就能看到最新的值了。
- 讀volatile變量時,會從主內(nèi)存中讀取最新的值,而不是從CPU緩存中讀取,這樣就能保證不會拿到過期的值了。
此外,由于lock前綴指令會對指定的內(nèi)存區(qū)域加鎖,保證了對該變量的讀寫操作的原子性,避免了出現(xiàn)競態(tài)條件。
5.2、不同內(nèi)存屏障的作用
對于內(nèi)存屏障的分類其實分有兩種,其中一種常見的便是對內(nèi)存屏障的粗分:
- 讀屏障:用于確保在讀取共享變量之前,先要讀取該變量之前的所有操作的結(jié)果;
- 寫屏障:用于確保在寫入共享變量之后,后續(xù)的所有操作都不能被重排序到寫操作之前。
細分之下,內(nèi)存屏障又分為四種:
- LoadLoad屏障:
- 保證在讀取共享變量之前,先要讀取該變量之前的所有操作的結(jié)果。
- 指令
Load1; LoadLoad; Load2
,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- LoadStore屏障:
- 保證在讀取共享變量之前,先要讀取該變量之前的所有操作的結(jié)果,并且在寫入共享變量之后,后續(xù)的所有操作都不能被重排序到寫操作之前。
- 指令
Load1; LoadStore; Store2
,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
- StoreStore屏障:
- 保證在寫入共享變量之后,后續(xù)的所有寫操作都不能被重排序到寫操作之前。
- 指令
Store1; StoreStore; Store2
,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
- StoreLoad屏障:
- 保證在寫入共享變量之后,后續(xù)的所有讀操作都不能被重排序到寫操作之前。
- 指令
Store1; StoreLoad; Load2
,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數(shù)處理器的實現(xiàn)中,這個屏障是個萬能屏障,兼具其它三種內(nèi)存屏障的功能
對于volatile操作而言,其操作步驟如下:
- 每個volatile寫入之前,插入一個 StoreStore,寫入以后插入一個 StoreLoad
- 每個volatile讀取之前,插入一個 LoadLoad,讀取之后插入一個 LoadStore
6、volatile和synchronized的區(qū)別
volatile和synchronized都可以保證多線程之間的可見性和原子性,但是它們之間有以下幾點不同:
- volatile只能保證可見性和有序性,不能保證原子性。而synchronized既可以保證可見性和有序性,也可以保證原子性。
- volatile不會阻塞線程,而synchronized會阻塞線程。
- volatile只能修飾變量,而synchronized可以修飾方法和代碼塊。
- volatile只能保證單次讀/寫的原子性,不能保證多次讀/寫的原子性。而synchronized可以保證多次讀/寫的原子性。
可見性保證 | 原子性保證 | 有序性保證 | 阻塞線程 | 可修飾對象 | 多次操作原子性 | |
---|---|---|---|---|---|---|
volatile(輕量) | ?? | ? | ? | ? | 變量 | ? |
synchronized(重量) | ?? | ?? | ?? | ?? | 方法、代碼塊 | ?? |
7、使用場景
7.1、多線程共享變量
在多線程環(huán)境下,多個線程可能同時訪問同一個變量。如果這個變量沒有被聲明為 volatile
,那么每個線程都會從自己的緩存中讀取這個變量的值,而不是從主內(nèi)存中讀取。這樣就可能會出現(xiàn)一個線程修改了變量的值,但是其他線程并沒有及時得到變量的更新,導致程序出現(xiàn)錯誤。
使用 volatile
聲明變量可以保證每個線程都從主內(nèi)存中讀取變量的值,而不是從自己的緩存中讀取。這樣就可以保證多個線程訪問同一個變量時的可見性和正確性。
7.2、雙重檢查鎖定
雙重檢查鎖定(Double-checked locking)是一種延遲初始化的技術(shù),常用于單例模式的實現(xiàn)。在雙重檢查鎖定模式中,首先檢查是否已經(jīng)實例化,如果沒有實例化,則進行同步代碼塊,再次檢查是否已經(jīng)實例化,如果沒有則進行實例化。
但是在沒有使用volatile修飾共享變量的情況下,可能會出現(xiàn)線程安全問題。因為在實例化對象時,可能會出現(xiàn)指令重排的情況,導致其他線程在檢查對象是否為null時,得到的是一個尚未完全初始化的對象。
使用volatile聲明共享變量可以禁止指令重排,從而保證雙重檢查鎖定模式的正確性。
7.3、狀態(tài)標志
當一個變量被用于表示某個狀態(tài)時,例如線程是否終止、是否可以執(zhí)行某項操作等,需要使用volatile來保證操作的可見性和正確性。
在多線程環(huán)境下,一個線程修改了狀態(tài)變量的值,其他線程需要及時得到變量的更新,以保證程序的正確性。