Java多線程之volatile關(guān)鍵字及內(nèi)存屏障實(shí)例解析
前面一篇文章在介紹Java內(nèi)存模型的三大特性(原子性、可見性、有序性)時,在可見性和有序性中都提到了volatile關(guān)鍵字,那這篇文章就來介紹volatile關(guān)鍵字的內(nèi)存語義以及實(shí)現(xiàn)其特性的內(nèi)存屏障。
volatile是JVM提供的一種最輕量級的同步機(jī)制,因?yàn)镴ava內(nèi)存模型為volatile定義特殊的訪問規(guī)則,使其可以實(shí)現(xiàn)Java內(nèi)存模型中的兩大特性:可見性和有序性。正因?yàn)関olatile關(guān)鍵字具有這兩大特性,所以我們可以使用volatile關(guān)鍵字解決多線程中的某些同步問題。
volatile的可見性
volatile的可見性是指當(dāng)一個變量被volatile修飾后,這個變量就對所有線程均可見。白話點(diǎn)就是說當(dāng)一個線程修改了一個volatile修飾的變量后,其他線程可以立刻得知這個變量的修改,拿到最這個變量最新的值。
結(jié)合前一篇文章提到的Java內(nèi)存模型中線程、工作內(nèi)存、主內(nèi)存的交互關(guān)系,我們對volatile的可見性也可以這么理解,定義為volatile修飾的變量,在線程對其進(jìn)行寫入操作時不會把值緩存在工作內(nèi)存中,而是直接把修改后的值刷新回寫到主內(nèi)存,而當(dāng)處理器監(jiān)控到其他線程中該變量在主內(nèi)存中的內(nèi)存地址發(fā)生變化時,會讓這些線程重新到主內(nèi)存中拷貝這個變量的最新值到工作內(nèi)存中,而不是繼續(xù)使用工作內(nèi)存中舊的緩存。
下面我列舉一個利用volatile可見性解決多線程并發(fā)安全的示例:
public class VolatileDemo { //private static boolean isReady = false; private static volatile boolean isReady = false; static class ReadyThread extends Thread { public void run() { while (!isReady) { } System.out.println("ReadyThread finish"); } } public static void main(String[] args) throws InterruptedException { new ReadyThread().start(); Thread.sleep(1000);//sleep 1秒鐘確保ReadyThread線程已經(jīng)開始執(zhí)行 isReady = true; } }
上面這段代碼運(yùn)行之后最終會在控制臺打印出: ReadyThread finish ,而當(dāng)你將變量isReady的volatile修飾符去掉之后再運(yùn)行則會發(fā)現(xiàn)程序一直運(yùn)行而不結(jié)束,而控制臺也沒有任何打印輸出。
我們分析下這個程序:初始時isReady為false,所以ReadyThread線程啟動開始執(zhí)行后,它的while代碼塊因標(biāo)志位isReady為false會進(jìn)入死循環(huán),當(dāng)用volatile關(guān)鍵字修飾isReady時,main方法所在的線程將isReady修改為true之后,ReadyThread線程會立刻得知并獲取這個最新的isReady值,緊接著while循環(huán)就會結(jié)束循環(huán),所以最后打印出了相關(guān)文字。而當(dāng)未用volatile修飾時,main方法所在的線程雖然修改了isReady變量,但ReadyThread線程并不知道這個修改,所以使用的還是之前的舊值,因此會一直死循環(huán)執(zhí)行while語句。
volatile的有序性
有序性是指程序代碼的執(zhí)行是按照代碼的實(shí)現(xiàn)順序來按序執(zhí)行的。
volatile的有序性特性則是指禁止JVM指令重排優(yōu)化。
我們來看一個例子:
public class Singleton { private static Singleton instance = null; //private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { //第一次判斷 if(instance == null) { synchronized (Singleton.class) { if(instance == null) { //初始化,并非原子操作 instance = new Singleton(); } } } return instance; } }
上面的代碼是一個很常見的單例模式實(shí)現(xiàn)方式,但是上述代碼在多線程環(huán)境下是有問題的。為什么呢,問題出在instance對象的初始化上,因?yàn)?code> instance = new Singleton(); 這個初始化操作并不是原子的,在JVM上會對應(yīng)下面的幾條指令:
memory =allocate(); //1. 分配對象的內(nèi)存空間 ctorInstance(memory); //2. 初始化對象 instance =memory; //3. 設(shè)置instance指向剛分配的內(nèi)存地址
上面三個指令中,步驟2依賴步驟1,但是步驟3不依賴步驟2,所以JVM可能針對他們進(jìn)行指令重拍序優(yōu)化,重排后的指令如下:
memory =allocate(); //1. 分配對象的內(nèi)存空間 instance =memory; //3. 設(shè)置instance指向剛分配的內(nèi)存地址 ctorInstance(memory); //2. 初始化對象
這樣優(yōu)化之后,內(nèi)存的初始化被放到了instance分配內(nèi)存地址的后面,這樣的話當(dāng)線程1執(zhí)行步驟3這段賦值指令后,剛好有另外一個線程2進(jìn)入getInstance方法判斷instance不為null,這個時候線程2拿到的instance對應(yīng)的內(nèi)存其實(shí)還未初始化,這個時候拿去使用就會導(dǎo)致出錯。
所以我們在用這種方式實(shí)現(xiàn)單例模式時,會使用volatile關(guān)鍵字修飾instance變量,這是因?yàn)関olatile關(guān)鍵字除了可以保證變量可見性之外,還具有防止指令重排序的作用。當(dāng)用volatile修飾instance之后,JVM執(zhí)行時就不會對上面提到的初始化指令進(jìn)行重排序優(yōu)化,這樣也就不會出現(xiàn)多線程安全問題了。
volatile使用場景
volatile的可以在以下場景中使用:
當(dāng)運(yùn)算結(jié)果不依賴變量當(dāng)前的值,或者能確保只有單一線程修改變量的值的時候,我們才可以對該變量使用volatile關(guān)鍵字
變量不需要與其他狀態(tài)變量共同參與不變約束
volatile與原子性
volatile關(guān)鍵字能保證變量的可見性和代碼的有序性,但是不能保證變量的原子性,下面我再舉一個volatile與原子性的例子:
public class VolatileTest { public static volatile int count = 0; public static void increase() { count++; } public static void main(String[] args) { Thread[] threads = new Thread[20]; for(int i = 0; i < threads.length; i++) { threads[i] = new Thread(() -> { for(int j = 0; j < 1000; j++) { increase(); } }); threads[i].start(); } //等待所有累加線程結(jié)束 while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(count); } }
上面這段代碼創(chuàng)建了20個線程,每個線程對變量count進(jìn)行1000次自增操作,如果這段代碼并發(fā)正常的話,結(jié)果應(yīng)該是20000,但實(shí)際運(yùn)行過程中經(jīng)常會出現(xiàn)小于20000的結(jié)果,因?yàn)閏ount++這個自增操作不是原子操作。
上面的count++自增操作等價于count=count+1,所以JVM需要先讀取count的值,然后在count的基礎(chǔ)上給它加1,然后再將新的值重新賦值給count變量,所以這個自增總共需要三步。
上圖中我將線程對count的自增操作畫了個簡單的流程,一個線程要對count進(jìn)行自增時要先讀取count的值,然后在當(dāng)前count值的基礎(chǔ)上進(jìn)行count+1操作,最后將count的新值重新寫回到count。
如果線程2在線程1讀取count舊值寫回count新值期間讀取count的值,顯然這個時候線程2讀取的是count還未更新的舊值,這時兩個線程是對同一個值進(jìn)行了+1操作,這樣這兩個線程就沒有對count實(shí)現(xiàn)累加效果,相反這些操作卻又沒有違反volatile的定義,所以這種情況下使用volatile依然會存在多線程并發(fā)安全的問題。
volatile與內(nèi)存屏障
前面介紹了volatile的可見性和有序性,那JVM到底是如何為volatile關(guān)鍵字實(shí)現(xiàn)的這兩大特性呢,Java內(nèi)存模型其實(shí)是通過內(nèi)存屏障(Memory Barrier)來實(shí)現(xiàn)的。
內(nèi)存屏障其實(shí)也是一種JVM指令,Java內(nèi)存模型的重排規(guī)則會要求Java編譯器在生成JVM指令時插入特定的內(nèi)存屏障指令,通過這些內(nèi)存屏障指令來禁止特定的指令重排序。
另外內(nèi)存屏障還具有一定的語義:內(nèi)存屏障之前的所有寫操作都要回寫到主內(nèi)存,內(nèi)存屏障之后的所有讀操作都能獲得內(nèi)存屏障之前的所有寫操作的最新結(jié)果(實(shí)現(xiàn)了可見性)。因此重排序時,不允許把內(nèi)存屏障之后的指令重排序到內(nèi)存屏障之前。
下面的表是volatile有關(guān)的禁止指令重排的行為:
第一個操作 | 第二個操作:普通讀寫 | 第二個操作:volatile讀 | 第二個操作:volatile寫 |
---|---|---|---|
普通讀寫 | 可以重排 | 可以重排 | 不可以重排 |
volatile讀 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile寫 | 可以重排 | 不可以重排 | 不可以重排 |
從上面的表我們可以得出下面這些結(jié)論:
當(dāng)?shù)诙€操作volatile寫時,不論第一個操作是什么,都不能重排序。這個規(guī)則保證了volatile寫之前的操作不會被重排到volatile寫之后。
當(dāng)?shù)谝粋€操作為volatile讀時,不論第二個操作是什么,都不能重排。這個操作保證了volatile讀之后的操作不會被重排到volatile讀之前。
當(dāng)?shù)谝粋€操作為volatile寫,第二個操作為volatile讀時,不能重排。
JVM中提供了四類內(nèi)存屏障指令:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保證load1的讀取操作在load2及后續(xù)讀取操作之前執(zhí)行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其后的寫操作執(zhí)行前,保證store1的寫操作已刷新到主內(nèi)存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的寫操作執(zhí)行前,保證load1的讀操作已讀取結(jié)束 |
StoreLoad | Store1; StoreLoad; Load2 | 保證store1的寫操作已刷新到主內(nèi)存之后,load2及其后的讀操作才能執(zhí)行 |
總結(jié)
volatile實(shí)現(xiàn)了Java內(nèi)存模型中的可見性和有序性,它的這兩大特性則是通過內(nèi)存屏障來實(shí)現(xiàn)的,同時volatile無法保證原子性。
以上所述是小編給大家介紹的Java多線程之volatile關(guān)鍵字及內(nèi)存屏障實(shí)例解析,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
Java使用Redis實(shí)現(xiàn)微博熱搜功能
在社交平臺上,熱搜功能是一個非常重要的組成部分,它展示了當(dāng)前最熱門的話題,幫助用戶迅速了解最受關(guān)注的事件,Redis 是一個高性能的鍵值存儲系統(tǒng),通常用于緩存和實(shí)時數(shù)據(jù)存儲,本文將通過 Java 結(jié)合 Redis 實(shí)現(xiàn)一個簡化版的微博熱搜功能,需要的朋友可以參考下2024-12-12詳解SpringBoot中@PostMapping注解的用法
在SpringBoot中,我們經(jīng)常需要編寫RESTful Web服務(wù),以便于客戶端與服務(wù)器之間的通信,@PostMapping注解可以讓我們更方便地編寫POST請求處理方法,在本文中,我們將介紹@PostMapping注解的作用、原理,以及如何在SpringBoot應(yīng)用程序中使用它2023-06-06Spring Boot中RedisTemplate的使用示例詳解
RedisTemplate.opsForHash()是RedisTemplate類提供的用于操作Hash類型的方法,它可以用于對Redis中的Hash數(shù)據(jù)結(jié)構(gòu)進(jìn)行各種操作,如設(shè)置字段值、獲取字段值、刪除字段值等,本文介紹Spring Boot中RedisTemplate的使用,感興趣的朋友一起看看吧2023-10-10SpringMVC項(xiàng)目異常處理機(jī)制詳解
SpringMVC是一種基于Java,實(shí)現(xiàn)了Web MVC設(shè)計模式,請求驅(qū)動類型的輕量級Web框架,即使用了MVC架構(gòu)模式的思想,將Web層進(jìn)行職責(zé)解耦?;谡埱篁?qū)動指的就是使用請求-響應(yīng)模型,框架的目的就是幫助我們簡化開發(fā),SpringMVC也是要簡化我們?nèi)粘eb開發(fā)2022-08-08Idea跑的項(xiàng)目沒問題將程序install成jar包運(yùn)行報錯空指針的問題
這篇文章主要介紹了Idea跑的項(xiàng)目沒問題,將程序install成jar包運(yùn)行報錯空指針的問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06Java中用戶向系統(tǒng)傳遞參數(shù)的三種基本方式實(shí)例分享
這篇文章主要介紹了Java中用戶向系統(tǒng)傳遞參數(shù)的三種基本方式實(shí)例,有需要的朋友可以參考一下2014-01-01教你開發(fā)腳手架集成Spring?Boot?Actuator監(jiān)控的詳細(xì)過程
這篇文章主要介紹了開發(fā)腳手架集成Spring?Boot?Actuator監(jiān)控的詳細(xì)過程,集成包括引入依賴配置文件及訪問驗(yàn)證的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-05-05