偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Volatile:JVM 我警告你,我的人你別亂動(dòng)

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

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

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

指令重排序

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

DCL,Double Check Look

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

面試官:用過單例嗎?

你:用過。

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

你:DCL。

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

你:加 Volatile。

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

但這個(gè)問題,還有一些可挖掘的內(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();

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

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

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

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

Object o = new Object();

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

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

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

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

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

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

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

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

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;        }    }}

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

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

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

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

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

運(yùn)行程序,經(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)開啟一個(gè)子線程,子線程中一個(gè) while 循環(huán),當(dāng) flag 為 false 時(shí),結(jié)束循環(huán)。flag 初始值為 true,一秒鐘后,被主線程設(shè)置為 false。

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

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

private static volatile boolean flag = true;

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

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

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

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

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

總結(jié)

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

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

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

2022-08-19 08:17:36

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

2020-12-30 09:18:46

JVM內(nèi)部信息

2019-08-02 17:48:16

戴爾

2022-02-15 20:08:41

JDKJavaWindows

2010-05-14 11:37:46

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

2019-01-07 08:59:01

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

2011-02-23 10:45:51

IT人才

2010-06-03 15:30:01

Windows2008

2020-02-04 16:37:17

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

2012-08-15 10:50:51

IE6

2023-11-18 09:17:56

Optional代碼

2015-10-28 17:35:35

自動(dòng)化運(yùn)維Ansible配置管理

2018-04-06 09:42:39

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

2022-12-01 17:17:09

React開發(fā)

2009-07-31 19:51:47

云計(jì)算

2022-03-15 09:58:12

單例模式系統(tǒng)

2022-09-13 11:50:21

Linux運(yùn)維命令行

2015-04-16 13:41:24

2022-04-29 08:00:36

web3區(qū)塊鏈比特幣
點(diǎn)贊
收藏

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