溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java指令重排序在多線程環(huán)境下如何處理

發(fā)布時(shí)間:2022-04-24 10:10:14 來源:億速云 閱讀:154 作者:zzz 欄目:開發(fā)技術(shù)

這篇文章主要講解了“Java指令重排序在多線程環(huán)境下如何處理”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Java指令重排序在多線程環(huán)境下如何處理”吧!

    一、序言

    指令重排在單線程環(huán)境下有利于提高程序的執(zhí)行效率,不會(huì)對(duì)程序產(chǎn)生負(fù)面影響;在多線程環(huán)境下,指令重排會(huì)給程序帶來意想不到的錯(cuò)誤。

    二、問題復(fù)原

    (一)關(guān)聯(lián)變量

    下面給出一個(gè)能夠百分之百?gòu)?fù)原指令重排的例子。

    ublic class D {
        static Integer a;
        static Boolean flag;
        
        public static void writer() {
            a = 1;
            flag = true;
        }
        public static void reader() {
            if (flag != null && flag) {
                System.out.println(a);
                a = 0;
                flag = false;
            }
    }
    1、結(jié)果預(yù)測(cè)

    reader方法僅在flag變量為true時(shí)向控制臺(tái)打印變量a的值。

    writer方法先執(zhí)行變量a的賦值操作,后執(zhí)行變量flag的賦值操作。

    如果按照上述分析邏輯,那么控制臺(tái)打印的結(jié)果一定全為1。

    2、指令重排

    假如代碼未發(fā)生指令重排,那么當(dāng)flag變量為true時(shí),變量a一定為1。

    上述代碼中關(guān)于變量a和變量flag在兩個(gè)方法類均存在指令重排的情況。

    public static void writer() {
        a = 1;
        flag = true;
    }

    通過觀察日志輸出,發(fā)現(xiàn)有大量的0輸出。

    當(dāng)writer方法內(nèi)部發(fā)生指令重排時(shí),flag變量先完成賦值,此時(shí)假如當(dāng)前線程發(fā)生中斷,其它線程在調(diào)用reader方法,檢測(cè)到flag變量為true,那么便打印變量a的值。此時(shí)控制臺(tái)存在超出期望值的結(jié)果。

    (二)new創(chuàng)建對(duì)象

    使用關(guān)鍵字new創(chuàng)建對(duì)象時(shí),因其非原子操作,故存在指令重排,指令重排在多線程環(huán)境下會(huì)帶來負(fù)面影響。

    public class Singleton {
        private static UserModel instance;
        
        public static UserModel getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new UserModel(2, "B");
                    }
                }
            }
            return instance;
        }
    }
    
    @Data
    @AllArgsConstructor
    class UserModel {
        private Integer userId;
        private String userName;
    1、解析創(chuàng)建過程
    • 使用關(guān)鍵字new創(chuàng)建一個(gè)對(duì)象,大致分為一下過程:

    • 在??臻g創(chuàng)建引用地址

    • 以類文件為模版在堆空間對(duì)象分配內(nèi)存

    • 成員變量初始化

    • 使用構(gòu)造函數(shù)初始化

    • 將引用值賦值給左側(cè)存儲(chǔ)變量

    2、重排序過程分析

    針對(duì)上述示例,假設(shè)第一個(gè)線程進(jìn)入synchronized代碼塊,并開始創(chuàng)建對(duì)象,由于重排序存在,正常的創(chuàng)建對(duì)象過程被打亂,可能會(huì)出現(xiàn)在??臻g創(chuàng)建引用地址后,將引用值賦值給左側(cè)存儲(chǔ)變量,隨后因CPU調(diào)度時(shí)間片耗盡而產(chǎn)生中斷的情況。

    后續(xù)線程在檢測(cè)到instance變量不為空,則直接使用。因?yàn)閱卫龑?duì)象并為實(shí)例化完成,直接使用會(huì)帶來意想不到的結(jié)果。

    三、應(yīng)對(duì)指令重排

    (一)AtomicReference原子類

    使用原子類將一組相關(guān)聯(lián)的變量封裝成一個(gè)對(duì)象,利用原子操作的特性,有效回避指令重排問題。

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class ValueModel {
        private Integer value;
        private Boolean flag;
    }

    原子類應(yīng)該是解決多線程環(huán)境下指令重排的首選方案,不僅通俗易懂,而且線程間使用的非重量級(jí)互斥鎖,效率相對(duì)較高。

    public class E {
        private static final AtomicReference<ValueModel> ar = new AtomicReference<>(new ValueModel());
        
        public static void writer() {
            ar.set(new ValueModel(1, true));
        }
        
        public static void reader() {
            ValueModel valueModel = ar.get();
            if (valueModel.getFlag() != null && valueModel.getFlag()) {
                System.out.println(valueModel.getValue());
                ar.set(new ValueModel(0, false));
            }
        }
    }

    當(dāng)一組相關(guān)聯(lián)的變量發(fā)生指令重排時(shí),使用原子操作類是比較優(yōu)的解法。

    (二)volatile關(guān)鍵字

    public class Singleton {
        private volatile static UserModel instance;
        
        public static UserModel getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new UserModel(2, "B");
                    }
                }
            }
            return instance;
        }
    }
    
    @Data
    @AllArgsConstructor
    class UserModel {
        private Integer userId;
        private String userName;

    四、指令重排的理解

    1、指令重排廣泛存在

    指令重排不僅限于Java程序,實(shí)際上各種編譯器均有指令重排的操作,從軟件到CPU硬件都有。指令重排是對(duì)單線程執(zhí)行的程序的一種性能優(yōu)化,需要明確的是,指令重排在單線程環(huán)境下,不會(huì)改變順序程序執(zhí)行的預(yù)期結(jié)果。

    2、多線程環(huán)境指令重排

    上面討論了兩種典型多線程環(huán)境下指令重排,分析其帶來負(fù)面影響,并分別提供了應(yīng)對(duì)方式。

    • 對(duì)于關(guān)聯(lián)變量,先封裝成一個(gè)對(duì)象,然后使用原子類來操作

    • 對(duì)于new對(duì)象,使用volatile關(guān)鍵字修飾目標(biāo)對(duì)象即可

    3、synchronized鎖與重排序無關(guān)

    synchronized鎖通過互斥鎖,有序的保證線程訪問特定的代碼塊。代碼塊內(nèi)部的代碼正常按照編譯器執(zhí)行的策略重排序。

    盡管synchronized鎖能夠回避多線程環(huán)境下重排序帶來的不利影響,但是互斥鎖帶來的線程開銷相對(duì)較大,不推薦使用。

    synchronized 塊里的非原子操作依舊可能發(fā)生指令重排

    感謝各位的閱讀,以上就是“Java指令重排序在多線程環(huán)境下如何處理”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)Java指令重排序在多線程環(huán)境下如何處理這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI