Java指令重排在多線程環(huán)境下的解決方式
一、序言
指令重排在單線程環(huán)境下有利于提高程序的執(zhí)行效率,不會(huì)對(duì)程序產(chǎn)生負(fù)面影響;在多線程環(huán)境下,指令重排會(huì)給程序帶來(lái)意想不到的錯(cuò)誤。
本文對(duì)多線程指令重排問(wèn)題進(jìn)行復(fù)原,并針對(duì)指令重排給出相應(yīng)的解決方案。
二、問(wèn)題復(fù)原
(一)關(guān)聯(lián)變量
下面給出一個(gè)能夠百分之百?gòu)?fù)原指令重排的例子。
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、結(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;
}通過(guò)觀察日志輸出,發(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ì)帶來(lái)負(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ò)程
- 使用關(guān)鍵字new創(chuàng)建一個(gè)對(duì)象,大致分為一下過(guò)程:
- 在??臻g創(chuàng)建引用地址
- 以類文件為模版在堆空間對(duì)象分配內(nèi)存
- 成員變量初始化
- 使用構(gòu)造函數(shù)初始化
- 將引用值賦值給左側(cè)存儲(chǔ)變量
2、重排序過(guò)程分析
針對(duì)上述示例,假設(shè)第一個(gè)線程進(jìn)入synchronized代碼塊,并開(kāi)始創(chuàng)建對(duì)象,由于重排序存在,正常的創(chuàng)建對(duì)象過(guò)程被打亂,可能會(huì)出現(xiàn)在棧空間創(chuàng)建引用地址后,將引用值賦值給左側(cè)存儲(chǔ)變量,隨后因CPU調(diào)度時(shí)間片耗盡而產(chǎn)生中斷的情況。
后續(xù)線程在檢測(cè)到instance變量不為空,則直接使用。因?yàn)閱卫龑?duì)象并為實(shí)例化完成,直接使用會(huì)帶來(lái)意想不到的結(jié)果。
三、應(yīng)對(duì)指令重排
(一)AtomicReference原子類
使用原子類將一組相關(guān)聯(lián)的變量封裝成一個(gè)對(duì)象,利用原子操作的特性,有效回避指令重排問(wèn)題。
@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)境下指令重排,分析其帶來(lái)負(fù)面影響,并分別提供了應(yīng)對(duì)方式。
- 對(duì)于關(guān)聯(lián)變量,先封裝成一個(gè)對(duì)象,然后使用原子類來(lái)操作
- 對(duì)于new對(duì)象,使用volatile關(guān)鍵字修飾目標(biāo)對(duì)象即可
3、synchronized鎖與重排序無(wú)關(guān)
synchronized鎖通過(guò)互斥鎖,有序的保證線程訪問(wèn)特定的代碼塊。代碼塊內(nèi)部的代碼正常按照編譯器執(zhí)行的策略重排序。
盡管synchronized鎖能夠回避多線程環(huán)境下重排序帶來(lái)的不利影響,但是互斥鎖帶來(lái)的線程開(kāi)銷相對(duì)較大,不推薦使用。
synchronized 塊里的非原子操作依舊可能發(fā)生指令重排
到此這篇關(guān)于Java多線程環(huán)境指令重排的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java實(shí)現(xiàn)Excel導(dǎo)入導(dǎo)出的步驟詳解
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)Excel的導(dǎo)入、導(dǎo)出,文中示例代碼介紹的非常詳細(xì),對(duì)我們的學(xué)習(xí)或工作有一定的幫助,感興趣的小伙伴們可以參考一下2023-06-06
淺談關(guān)于Mybatis的mapper-locations配置問(wèn)題
MyBatis 是一款優(yōu)秀的半自動(dòng)的ORM持久層框架,它支持自定義 SQL、存儲(chǔ)過(guò)程以及高級(jí)映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作,需要的朋友可以參考下2023-05-05
FeignMultipartSupportConfig上傳圖片配置方式
這篇文章主要介紹了FeignMultipartSupportConfig上傳圖片配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
springboot 2.x整合mybatis實(shí)現(xiàn)增刪查和批量處理方式
這篇文章主要介紹了springboot 2.x整合mybatis實(shí)現(xiàn)增刪查和批量處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
開(kāi)源的Java圖片處理庫(kù)實(shí)例詳解
Java?圖片處理庫(kù)提供了豐富的功能,用于處理和增強(qiáng)圖像,在Java生態(tài)系統(tǒng)中,有幾個(gè)流行的開(kāi)源庫(kù)可以用于圖片處理,這些庫(kù)提供了豐富的功能,如圖像縮放、裁剪、顏色調(diào)整、格式轉(zhuǎn)換等,本文介紹開(kāi)源的Java圖片處理庫(kù)介紹,感興趣的朋友一起看看吧2024-03-03
Java httpclient請(qǐng)求form-data格式并設(shè)置boundary代碼實(shí)現(xiàn)方法
在 Java 開(kāi)發(fā)中,經(jīng)常會(huì)遇到需要使用 httpclient 發(fā)送 form-data 格式請(qǐng)求的場(chǎng)景,本文將詳細(xì)介紹如何正確地實(shí)現(xiàn)這一操作,包括數(shù)據(jù)格式示例、常見(jiàn)報(bào)錯(cuò)及解決方法,本文通過(guò)實(shí)例代碼詳解講解,感興趣的朋友一起看看吧2025-01-01
mybatis plus 關(guān)聯(lián)數(shù)據(jù)庫(kù)排除不必要字段方式
這篇文章主要介紹了mybatis plus 關(guān)聯(lián)數(shù)據(jù)庫(kù)排除不必要字段方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
關(guān)于遠(yuǎn)程調(diào)用RestTemplate的使用避坑指南
這篇文章主要介紹了關(guān)于遠(yuǎn)程調(diào)用RestTemplate的使用避坑指南,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10

