首頁  >  文章  >  Java  >  Java指令重排在多執行緒環境下怎麼解決

Java指令重排在多執行緒環境下怎麼解決

PHPz
PHPz轉載
2023-04-19 15:40:061261瀏覽

一、序言

指令重排在單執行緒環境下有利於提高程式的執行效率,不會對程式產生負面影響;在多執行緒環境下,指令重排會為程式帶來意想不到的錯誤。

二、問題復原

(一)關聯變數

下面給出一個能夠百分之百復原指令重排的例子。

public 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、結果預測

reader方法僅在flag變數為true時向控制台列印變數a的值。

writer方法先執行變數a的賦值運算,後來執行變數flag的賦值運算。

如果依照上述分析邏輯,那麼控制台列印的結果一定全為1。

2、指令重排

假如程式碼未發生指令重排,那麼當flag變數為true時,變數a一定為1。

上述程式碼中關於變數a和變數flag在兩個方法類別都存在指令重排的情況。

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

透過觀察日誌輸出,發現有大量的0輸出。

writer方法內部發生指令重排時,flag變數先完成賦值,此時假如當前執行緒發生中斷,其它執行緒在呼叫reader 方法,偵測到flag變數為true,那麼便列印變數a的值。此時控制台存在超出期望值的結果。

(二)new創建物件

使用關鍵字new建立物件時,因其非原子操作,故存在指令重排,指令重排在多執行緒環境下會帶來負面影響。

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、解析建立過程
  • 使用關鍵字new建立一個對象,大致分為一下過程:

  • 在堆疊空間建立參考位址

  • 以類別檔案為模版在堆疊空間物件中分配記憶體

  • ##成員變數初始化

  • #使用建構函式初始化

  • 將引用值賦值給左側儲存變數

  • ##2、重新排序過程分析
針對上述範例,假設第一個執行緒進入synchronized程式碼區塊,並開始建立對象,由於重排序存在,正常的建立物件過程被打亂,可能會出現在堆疊空間建立參考位址後,將引用值賦值給左側儲存變量,隨後因CPU調度時間片耗盡而產生中斷的情況。

後續執行緒在偵測到

instance

變數不為空,則直接使用。因為單例物件並為實例化完成,直接使用會帶來意想不到的結果。 三、應對指令重排

(一)AtomicReference原子類

使用原子類將一組相關聯的變數封裝成一個對象,利用原子操作的特性,有效迴避指令重排問題。

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

原子類別應該是解決多執行緒環境下指令重排的首選方案,不僅簡單易懂,而且執行緒間使用的非重量級互斥鎖,效率相對較高。

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

當一組相關聯的變數發生指令重排時,使用原子操作類別是比較優的解法。

(二)volatile關鍵字
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程序,實際上各種編譯器都有指令重排的操作,從軟體到CPU硬體都有。指令重排是對單執行緒執行的程式的一種效能最佳化,需要明確的是,指令重排在單執行緒環境下,不會改變順序程式執行的預期結果。

2、多執行緒環境指令重排

上面討論了兩種典型多執行緒環境下指令重排,分析其帶來負面影響,並分別提供了因應方式。

    對於關聯變量,先封裝成一個對象,然後使用原子類來操作
  • 對於new對象,使用volatile關鍵字修飾目標物件即可
  • 3、synchronized鎖定與重排序無關

synchronized鎖定透過互斥鎖,有序的保證執行緒存取特定的程式碼區塊。程式碼區塊內部的程式碼正常會依照編譯器執行的策略重新排序。

儘管synchronized鎖定能夠迴避多執行緒環境下重排序帶來的不利影響,但是互斥鎖帶來的執行緒開銷相對較大,不建議使用。

synchronized 區塊裡的非原子操作依舊可能發生指令重排

以上是Java指令重排在多執行緒環境下怎麼解決的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除