Java中volatile?的作用
前言:
volatile 是 Java 并發(fā)編程的重要組成部分,也是常見的面試題之一,它的主要作用有兩個(gè):保證內(nèi)存的可見性和禁止指令重排序。下面我們具體來(lái)看這兩個(gè)功能。
內(nèi)存可見性
說(shuō)到內(nèi)存可見性問(wèn)題就不得不提 Java 內(nèi)存模型,Java 內(nèi)存模型(Java Memory Model)簡(jiǎn)稱為 JMM,主要是用來(lái)屏蔽不同硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異的,因?yàn)樵诓煌挠布筒煌牟僮飨到y(tǒng)下,內(nèi)存的訪問(wèn)是有一定的差異得,這種差異會(huì)導(dǎo)致相同的代碼在不同的硬件和不同的操作系統(tǒng)下有著不一樣的行為,而 Java 內(nèi)存模型就是解決這個(gè)差異,統(tǒng)一相同代碼在不同硬件和不同操作系統(tǒng)下的差異的。
Java 內(nèi)存模型規(guī)定:所有的變量(實(shí)例變量和靜態(tài)變量)都必須存儲(chǔ)在主內(nèi)存中,每個(gè)線程也會(huì)有自己的工作內(nèi)存,線程的工作內(nèi)存保存了該線程用到的變量和主內(nèi)存的副本拷貝,線程對(duì)變量的操作都在工作內(nèi)存中進(jìn)行。線程不能直接讀寫主內(nèi)存中的變量,
如下圖所示:
然而,Java 內(nèi)存模型會(huì)帶來(lái)一個(gè)新的問(wèn)題,那就是內(nèi)存可見性問(wèn)題,也就是當(dāng)某個(gè)線程修改了主內(nèi)存中共享變量的值之后,其他線程不能感知到此值被修改了,它會(huì)一直使用自己工作內(nèi)存中的“舊值”,這樣程序的執(zhí)行結(jié)果就不符合我們的預(yù)期了,這就是內(nèi)存可見性問(wèn)題,我們用以下代碼來(lái)演示一下這個(gè)問(wèn)題:
private static boolean flag = false; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!flag) { } System.out.println("終止執(zhí)行"); } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("設(shè)置 flag=true"); flag = true; } }); t2.start(); }
以上代碼我們預(yù)期的結(jié)果是,在線程 1 執(zhí)行了 1s 之后,線程 2 將 flag 變量修改為 true,之后線程 1 終止執(zhí)行,然而,因?yàn)榫€程 1 感知不到 flag 變量發(fā)生了修改,也就是內(nèi)存可見性問(wèn)題,所以會(huì)導(dǎo)致線程 1 會(huì)永遠(yuǎn)的執(zhí)行下去,最終我們看到的結(jié)果是這樣的:
如何解決以上問(wèn)題呢?只需要給變量 flag 加上 volatile 修飾即可,具體的實(shí)現(xiàn)代碼如下:
private volatile static boolean flag = false; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { while (!flag) { } System.out.println("終止執(zhí)行"); } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("設(shè)置 flag=true"); flag = true; } }); t2.start(); }
以上程序的執(zhí)行結(jié)果如下圖所示:
禁止指令重排序
指令重排序是指編譯器或 CPU 為了優(yōu)化程序的執(zhí)行性能,而對(duì)指令進(jìn)行重新排序的一種手段。
指令重排序的實(shí)現(xiàn)初衷是好的,但是在多線程執(zhí)行中,如果執(zhí)行了指令重排序可能會(huì)導(dǎo)致程序執(zhí)行出錯(cuò)。指令重排序最典型的一個(gè)問(wèn)題就發(fā)生在單例模式中,
比如以下問(wèn)題代碼:
public class Singleton { private Singleton() {} private static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { // ① synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // ② } } } return instance; } }
以上問(wèn)題發(fā)生在代碼 ② 這一行“instance = new Singleton();”,這行代碼看似只是一個(gè)創(chuàng)建對(duì)象的過(guò)程,然而它的實(shí)際執(zhí)行卻分為以下 3 步:
- 創(chuàng)建內(nèi)存空間。
- 在內(nèi)存空間中初始化對(duì)象 Singleton。
- 將內(nèi)存地址賦值給 instance 對(duì)象(執(zhí)行了此步驟,instance 就不等于 null 了)。
如果此變量不加 volatile,那么線程 1 在執(zhí)行到上述代碼的第 ② 處時(shí)就可能會(huì)執(zhí)行指令重排序,將原本是 1、2、3 的執(zhí)行順序,重排為 1、3、2。但是特殊情況下,線程 1 在執(zhí)行完第 3 步之后,如果來(lái)了線程 2 執(zhí)行到上述代碼的第 ① 處,判斷 instance 對(duì)象已經(jīng)不為 null,但此時(shí)線程 1 還未將對(duì)象實(shí)例化完,那么線程 2 將會(huì)得到一個(gè)被實(shí)例化“一半”的對(duì)象,從而導(dǎo)致程序執(zhí)行出錯(cuò),這就是為什么要給私有變量添加 volatile 的原因了。
要使以上單例模式變?yōu)榫€程安全的程序,需要給 instance 變量添加 volatile 修飾,它的最終實(shí)現(xiàn)代碼如下:
public class Singleton { private Singleton() {} // 使用 volatile 禁止指令重排序 private static volatile Singleton instance = null; // 【主要是此行代碼發(fā)生了變化】 public static Singleton getInstance() { if (instance == null) { // ① synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // ② } } } return instance; } }
總結(jié)
volatile 是 Java 并發(fā)編程的重要組成部分,它的主要作用有兩個(gè):保證內(nèi)存的可見性和禁止指令重排序。volatile 常使用在一寫多讀的場(chǎng)景中,比如 CopyOnWriteArrayList 集合,它在操作的時(shí)候會(huì)把全部數(shù)據(jù)復(fù)制出來(lái)對(duì)寫操作加鎖,修改完之后再使用 setArray 方法把此數(shù)組賦值為更新后的值,使用 volatile 可以使讀線程很快的告知到數(shù)組被修改,不會(huì)進(jìn)行指令重排,操作完成后就可以對(duì)其他線程可見了。
到此這篇關(guān)于Java中volatile 的作用的文章就介紹到這了,更多相關(guān)volatile的作用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java如何使用ReentrantLock實(shí)現(xiàn)長(zhǎng)輪詢
這篇文章主要介紹了如何使用ReentrantLock實(shí)現(xiàn)長(zhǎng)輪詢,對(duì)ReentrantLock感興趣的同學(xué),可以參考下2021-04-04jvm中指定時(shí)區(qū)信息user.timezone問(wèn)題及解決方式
同一份程序使用時(shí)間LocalDateTime類型,在國(guó)內(nèi)和國(guó)外部署后,返回的時(shí)間信息前端使用出問(wèn)題,這篇文章主要介紹了jvm中指定時(shí)區(qū)信息user.timezone問(wèn)題及解決方法,需要的朋友可以參考下2023-02-02SpringBoot中的ApplicationListener事件監(jiān)聽器使用詳解
這篇文章主要介紹了SpringBoot中的ApplicationListener事件監(jiān)聽器使用詳解,ApplicationListener是應(yīng)用程序的事件監(jiān)聽器,繼承自java.util.EventListener標(biāo)準(zhǔn)接口,采用觀察者設(shè)計(jì)模式,需要的朋友可以參考下2023-11-11java通過(guò)jni調(diào)用opencv處理圖像的方法
今天小編就為大家分享一篇java通過(guò)jni調(diào)用opencv處理圖像的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08Springboot之日志、配置文件、接口數(shù)據(jù)如何脫敏
本文主要介紹了Springboot之配置文件數(shù)據(jù)脫敏、接口返回?cái)?shù)據(jù)脫敏、日志文件數(shù)據(jù)脫敏三個(gè)方面,需要了解學(xué)習(xí)的小伙伴快跟隨小編的腳步一起去看看吧2021-09-09深入了解SpringBoot中@InitBinder注解的使用
這篇文章主要介紹了深入了解SpringBoot中@InitBinder注解的使用,@InitBinder注解可以作用在被@Controller注解的類的方法上,表示為當(dāng)前控制器注冊(cè)一個(gè)屬性編輯器,用于對(duì)WebDataBinder進(jìn)行初始化,且只對(duì)當(dāng)前的Controller有效,需要的朋友可以參考下2023-10-10SpringBoot2.0整合WebSocket代碼實(shí)例
這篇文章主要介紹了SpringBoot2.0整合WebSocket代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11