自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Volatile:JVM 我警告你,我的人你別亂動

開發(fā) 架構(gòu)
Volatile 的意思是,易變的,動蕩不定的,反復(fù)無常的。Volatile 的作用就是告訴 JVM,被我修飾的變量它非常善變,你要給我盯好了,一旦有風(fēng)吹草動要立馬通知大家;另外,你不要自作聰明的調(diào)整它的位置(為了性能重排序),它可是說翻臉就翻臉的主兒

Volatile 算是一個面試中的高頻問題了。我們都知道 Volatile 有兩個作用:

  1. 禁止指令重排
  2. 保證內(nèi)存可見

指令重排序

指令重排序的問題,基本上都是通過 DCL 問題來考察。

DCL,Double Check Look

面試中通常會是下面這種情景:

面試官:用過單例嗎?

你:用過。

面試官:如何實現(xiàn)一個線程安全的懶漢式單例

你:DCL。

面試官:DCL 可以保證線程絕對安全嗎?

你:加 Volatile。

面試官滿意的點點頭。通常情況下,面試中這個問題聊到這里也就結(jié)束了。

但這個問題,還有一些可挖掘的內(nèi)容。我們順著單例的代碼繼續(xù)往下挖:

public class Singleton {        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;    }}

如果不加 Volatile,會有什么問題呢?問題就出現(xiàn)在下面這行代碼:

instance = new Singleton();

上面這行代碼看起來也平平無奇呀,就是一個賦值操作,還能整什么幺蛾子呢?我們只寫了一行代碼,但 JVM 則需要做好幾步操作。那 JVM 究竟干了啥呢?大概也許可能差不多就是把大象給放冰箱里了。

Java 代碼中的一條賦值語句,到了 JVM 指令層面大概分三步:

  1. 分配一塊內(nèi)存空間
  2. 初始化
  3. 返回內(nèi)存地址

下面通過字節(jié)碼來一探究竟,為了簡化問題,我們替換成下面的代碼:

Object o = new Object();

編譯以后,通過 javap -v 命令,或者 IDEA 中的 JClassLib 插件可以看到如下圖所示的內(nèi)容:

通過上面的字節(jié)碼信息,可以更加清楚的看到上面提到的那三個步驟:

  1. new 用來分配一塊內(nèi)存空間
  2. invokspecial 調(diào)用了 Object 的 init() 方法,做了初始化
  3. astore_1 就是將 o 指向了 Object 實例對象的內(nèi)存地址,完成賦值

dup 指令會做一些入棧操作,跟我們要討論的問題關(guān)系不大,這里可以先忽略。

到這里,問題就比較明了了。重排的問題會發(fā)生在第 2 和 3 步。因為先初始化還是先把對象的內(nèi)存地址賦值給 o,并沒有必然的前后制約關(guān)系。因此,這類的指令在某些情況下會被重排序。

單線程下,這種重排序完全沒有問題。但是多線程的場景下,就有可能出問題:A 線程進入到 instance = new Singleton(); 后,由于指令重排,在 init 之前,將地址給了 o。此時 B 線程來了,發(fā)現(xiàn) instance 不為 null,于是直接拿去用了,然而此時 instance 并沒有初始化,只是個半成品。所以,當(dāng) B 拿到 instance 進行操作的時候就會出現(xiàn)問題了。

因此,instance 需要使用 volatile 來修飾,從而禁止進行指令重排。

到這里,你可能要說了,我用單例不加 volatile,這么長時間了也沒遇到你說的重排序問題。你怎么證明「重排序」的存在呢?好問題,下面咱們通過一個小例子來驗證一下重排序是否真的存在。

private static int x = 0;private static int y = 0;private static int a = 0;private static int b = 0;public static void main(String[] args) throws InterruptedException {    int i = 0;    while (true) {        i++;        x = 0; y = 0;        a = 0; b = 0;                Thread one = new Thread(() -> {            a = 1;            x = b;        });        Thread two = new Thread(() -> {            b = 1;            y = a;        });                one.start();        two.start();        one.join();        two.join();        if(x == 0 && y == 0) {            log.info("第 {} 次,x = {}, y = {}", i, x, y);            break;        }    }}

代碼很簡單,就是幾個賦值操作,但卻很巧妙。x、y、a、b 初始都為 0,兩個線程分別給 a、x 和 b、y 賦值,線程 one 先讓 a = 1,然后再讓 x = b;two 線程先讓 b = 1,然后再讓 y = a。

假如不發(fā)生重排序,那么以上程序只會有下面六種可能:

每一列,從上到下代表代碼執(zhí)行的順序。

也就是說,在沒有重排序的情況下,不可能出現(xiàn) x、y 同時為 0 的情況。而如果 x、y 同時為 0 了,那么一定是出現(xiàn)了下面六種情況中的一種,既發(fā)生了重排。

每一列,從上到下代表代碼執(zhí)行的順序。

運行程序,經(jīng)過漫長的等待,得到了如下的輸出:

可以看到,在執(zhí)行了五十多萬次以后,我們終于捕捉到了一次重排序。發(fā)生這種情況的幾率很低,所以你就算沒有用 volatile 大概率不會有問題,但我們在今后還是要合理的使用 volatile。

內(nèi)存可見性

聊完指令重排,接下來聊聊內(nèi)存可見。這次我們直接上代碼:

private static boolean flag = true;private static void justRun() {    System.out.println("Thread One Start");    while (flag) {}    System.out.println("Thread One End");}public static void main(String[] args) throws InterruptedException {    new Thread(() -> justRun(), "Thread One").start();    TimeUnit.SECONDS.sleep(1);    flag = false;}

代碼很簡單,主線程內(nèi)開啟一個子線程,子線程中一個 while 循環(huán),當(dāng) flag 為 false 時,結(jié)束循環(huán)。flag 初始值為 true,一秒鐘后,被主線程設(shè)置為 false。

按照上面這個邏輯,子線程應(yīng)該會在程序啟動一秒后停止。然而,當(dāng)你運行程序后會發(fā)現(xiàn),這個程序就像吃了炫邁一樣,根本停不下來。

這說明主線程對 flag 的修改,子線程并沒有感知到。我們修改一下程序:

private static volatile boolean flag = true;

為 flag 加上 volatile 修飾符,再次運行,你會發(fā)現(xiàn)程序運行后,很快(大概一秒鐘)就停止了。這是為啥?是炫邁的藥勁兒過了嗎?

哈哈,當(dāng)然不是。為了更好的性能,線程都有自己的緩存(CPU 中的高速緩存),我們稱之為工作內(nèi)存或者本地內(nèi)存。還有一塊公共內(nèi)存,我們叫它主從吧。它們的結(jié)構(gòu)大致如下圖所示:

主存中定義了一個 flag 變量,每個線程讀取它的時候,為了更好的性能會在線程本地緩存一份它的副本。讀取的時候也是優(yōu)先讀取本地副本的值。當(dāng) flag 被 volatile 修飾后,每次被修改,都會讓其他線程中的副本失效,從而必須去主存中讀取最新的值。所以,在使用了 volatile 后,子線程能夠立即感知到 flag 的變化,從而停止。

上圖簡化了線程(CPU)的緩存結(jié)構(gòu),其完整結(jié)構(gòu)如下圖所示:

現(xiàn)代 CPU 共有三級緩存,分別為:L1、L2 和 L3。CPU 中的每個核心都有自己的 L1 和 L2,而一顆 CPU 中的多個核心會共享 L3。

總結(jié)

Volatile 的意思是,易變的,動蕩不定的,反復(fù)無常的。volatile 的作用就是告訴 JVM,被我修飾的變量它非常善變,你要給我盯好了,一旦有風(fēng)吹草動要立馬通知大家;另外,你不要自作聰明的調(diào)整它的位置(為了性能重排序),它可是說翻臉就翻臉的主兒。

最后,留一個小問題:內(nèi)存可見性的那個程序中,就算 flag 沒有被 volatile 修飾,線程頂多不是第一時間讀到 flag 的修改,但也不應(yīng)該一直讀不到呀,這是為啥?這太反直覺了!

責(zé)任編輯:姜華 來源: 今日頭條
相關(guān)推薦

2022-08-19 08:17:36

JWT服務(wù)器身份信息

2020-12-30 09:18:46

JVM內(nèi)部信息

2022-02-15 20:08:41

JDKJavaWindows

2019-08-02 17:48:16

戴爾

2010-05-14 11:37:46

網(wǎng)絡(luò)攻擊黑客美國

2019-01-07 08:59:01

uCPEvCPE網(wǎng)絡(luò)

2010-06-03 15:30:01

Windows2008

2011-02-23 10:45:51

IT人才

2020-02-04 16:37:17

k8s 相關(guān)應(yīng)用

2009-07-31 19:51:47

云計算

2012-08-15 10:50:51

IE6

2022-09-13 11:50:21

Linux運維命令行

2022-03-15 09:58:12

單例模式系統(tǒng)

2018-04-06 09:42:39

Windows操作系統(tǒng)功能

2023-11-18 09:17:56

Optional代碼

2022-12-01 17:17:09

React開發(fā)

2015-10-28 17:35:35

自動化運維Ansible配置管理

2022-04-29 08:00:36

web3區(qū)塊鏈比特幣

2015-04-16 13:41:24

點贊
收藏

51CTO技術(shù)棧公眾號