java開發(fā)中為什么雙重效驗(yàn)鎖要加volatile
正文
單例模式的實(shí)現(xiàn)方法有很多種,如餓漢模式、懶漢模式、靜態(tài)內(nèi)部類和枚舉等,當(dāng)面試官問到“為什么雙重效驗(yàn)鎖要加volatile?”時(shí),那么他指的是為什么懶漢模式中的私有變量要加 volatile?
懶漢模式指的是對象的創(chuàng)建是懶加載的方式,并不是在程序啟動(dòng)時(shí)就創(chuàng)建對象,而是第一次被真正使用時(shí)才創(chuàng)建對象。
要解釋為什么要加 volatile?我們先來看懶漢模式的具體實(shí)現(xiàn)代碼:
public class Singleton { // 1.防止外部直接 new 對象破壞單例模式 private Singleton() {} // 2.通過私有變量保存單例對象【添加了 volatile 修飾】 private static volatile Singleton instance = null; // 3.提供公共獲取單例對象的方法 public static Singleton getInstance() { if (instance == null) { // 第 1 次效驗(yàn) synchronized (Singleton.class) { if (instance == null) { // 第 2 次效驗(yàn) instance = new Singleton(); } } } return instance; } }
從上述代碼可以看出,為了保證線程安全和高性能,代碼中使用了兩次 if 和 synchronized 來保證程序的執(zhí)行。那既然已經(jīng)有 synchronized 來保證線程安全了,為什么還要給變量加 volatile 呢?
在解釋這個(gè)問題之前,我們先要搞懂一個(gè)前置知識:volatile 有什么用呢?
1.volatile 作用
volatile 有兩個(gè)主要的作用,第一,解決內(nèi)存可見性問題,第二,防止指令重排序。
1.1 內(nèi)存可見性問題
所謂內(nèi)存可見性問題,指的是多個(gè)線程同時(shí)操作一個(gè)變量,其中某個(gè)線程修改了變量的值之后,其他線程感知不到變量的修改,這就是內(nèi)存可見性問題。
而使用 volatile 就可以解決內(nèi)存可見性問題,比如以下代碼,當(dāng)沒有添加 volatile 時(shí),它的實(shí)現(xiàn)如下:
private static boolean flag = false; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { // 如果 flag 變量為 true 就終止執(zhí)行 while (!flag) { } System.out.println("終止執(zhí)行"); } }); t1.start(); // 1s 之后將 flag 變量的值修改為 true 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é)果如下:
然而,以上程序執(zhí)行了 N 久之后,依然沒有結(jié)束執(zhí)行,這說明線程 2 在修改了 flag 變量之后,線程 1 根本沒有感知到變量的修改。
那么接下來,我們嘗試給 flag 加上 volatile,實(shí)現(xiàn)代碼如下:
public class volatileTest { private static volatile boolean flag = false; public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { // 如果 flag 變量為 true 就終止執(zhí)行 while (!flag) { } System.out.println("終止執(zhí)行"); } }); t1.start(); // 1s 之后將 flag 變量的值修改為 true 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é)果如下:
從上述執(zhí)行結(jié)果我們可以看出,使用 volatile 之后就可以解決程序中的內(nèi)存可見性問題了。
1.2 防止指令重排序
指令重排序是指在程序執(zhí)行過程中,編譯器或 JVM 常常會(huì)對指令進(jìn)行重新排序,已提高程序的執(zhí)行性能。
指令重排序的設(shè)計(jì)初衷確實(shí)很好,在單線程中也能發(fā)揮很棒的作用,然而在多線程中,使用指令重排序就可能會(huì)導(dǎo)致線程安全問題了。
所謂線程安全問題是指程序的執(zhí)行結(jié)果,和我們的預(yù)期不相符。比如我們預(yù)期的正確結(jié)果是 0,但程序的執(zhí)行結(jié)果卻是 1,那么這就是線程安全問題。
而使用 volatile 可以禁止指令重排序,從而保證程序在多線程運(yùn)行時(shí)能夠正確執(zhí)行。
2.為什么要用 volatile?
回到主題,我們在單例模式中使用 volatile,主要是使用 volatile 可以禁止指令重排序,從而保證程序的正常運(yùn)行。這里可能會(huì)有讀者提出疑問,不是已經(jīng)使用了 synchronized 來保證線程安全嗎?那為什么還要再加 volatile 呢?看下面的代碼:
public class Singleton { private Singleton() {} // 使用 volatile 禁止指令重排序 private static volatile Singleton instance = null; public static Singleton getInstance() { if (instance == null) { // ① synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); // ② } } } return instance; } }
注意觀察上述代碼,我標(biāo)記了第 ① 處和第 ② 處的兩行代碼。給私有變量加 volatile 主要是為了防止第 ② 處執(zhí)行時(shí),也就是“instance = new Singleton()”執(zhí)行時(shí)的指令重排序的,這行代碼看似只是一個(gè)創(chuàng)建對象的過程
然而它的實(shí)際執(zhí)行卻分為以下 3 步:
- 創(chuàng)建內(nèi)存空間。
- 在內(nèi)存空間中初始化對象 Singleton。
- 將內(nèi)存地址賦值給 instance 對象(執(zhí)行了此步驟,instance 就不等于 null 了)。
試想一下,如果不加 volatile,那么線程 1 在執(zhí)行到上述代碼的第 ② 處時(shí)就可能會(huì)執(zhí)行指令重排序,將原本是 1、2、3 的執(zhí)行順序,重排為 1、3、2。但是特殊情況下,線程 1 在執(zhí)行完第 3 步之后,如果來了線程 2 執(zhí)行到上述代碼的第 ① 處,判斷 instance 對象已經(jīng)不為 null,但此時(shí)線程 1 還未將對象實(shí)例化完,那么線程 2 將會(huì)得到一個(gè)被實(shí)例化“一半”的對象,從而導(dǎo)致程序執(zhí)行出錯(cuò),這就是為什么要給私有變量添加 volatile 的原因了。
小結(jié)
使用 volatile 可以解決內(nèi)存可見性問題和防止指令重排序,我們在單例模式中使用 volatile 主要是使用 volatile 的后一個(gè)特性(防止指令重排序),從而避免多線程執(zhí)行的情況下,因?yàn)橹噶钪嘏判蚨鴮?dǎo)致某些線程得到一個(gè)未被完全實(shí)例化的對象,從而導(dǎo)致程序執(zhí)行出錯(cuò)的情況。
以上就是java開發(fā)中為什么雙重效驗(yàn)鎖要加volatile的詳細(xì)內(nèi)容,更多關(guān)于java雙重效驗(yàn)鎖volatile的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot中GlobalExceptionHandler異常處理機(jī)制詳細(xì)說明
Spring Boot的GlobalExceptionHandler是一個(gè)全局異常處理器,用于捕獲和處理應(yīng)用程序中發(fā)生的所有異常,這篇文章主要給大家介紹了關(guān)于Java中GlobalExceptionHandler異常處理機(jī)制的相關(guān)資料,需要的朋友可以參考下2024-03-03SpringBoot同時(shí)啟動(dòng)不同端口圖示解析
這篇文章主要介紹了SpringBoot同時(shí)啟動(dòng)不同端口圖示解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02multi-catch和try-catch異常處理知識點(diǎn)詳解
在本篇文章里我們給大家分享了一篇關(guān)于multi-catch和try-catch異常處理知識點(diǎn)內(nèi)容,有需要的朋友們可以參考學(xué)習(xí)下。2019-11-11SpringBoot?Security使用MySQL實(shí)現(xiàn)驗(yàn)證與權(quán)限管理
安全管理是軟件系統(tǒng)必不可少的的功能。根據(jù)經(jīng)典的“墨菲定律”——凡是可能,總會(huì)發(fā)生。如果系統(tǒng)存在安全隱患,最終必然會(huì)出現(xiàn)問題,這篇文章主要介紹了SpringBoot安全管理Spring?Security基本配置2022-11-11