## 指令重排序 ### 1、問題描述 首先一定要明確:指令重排序和有序性是不一樣的。這一點非常重要。 我們經常都會這麼說: - volatile能保證記憶體可見性、禁止指令重排序但是不能保證原子性。 - synchronized能保證原子性、可見性和有序性。 > 註意:這裡的有序性並不是代表能禁止指 ...
指令重排序
1、問題描述
首先一定要明確:指令重排序和有序性是不一樣的。這一點非常重要。
我們經常都會這麼說:
- volatile能保證記憶體可見性、禁止指令重排序但是不能保證原子性。
- synchronized能保證原子性、可見性和有序性。
註意:這裡的有序性並不是代表能禁止指令重排序。
舉個例子:
在雙重檢查的單例模式中,既然已經加了synchronized為什麼還需要volatile去修飾變數呢?如果synchronized能禁止指令重排,那麼完全可以不用要volatile。
推薦一個開源免費的 Spring Boot 實戰項目:
2、DCL代碼位元組碼分析指令重排序問題
首先需要知道的知識點:Object obj = new Object();這句代碼並不是一個原子操作,他分為三步:
- 在記憶體申請一片空間,new 出一個對象
- 調用new出的這個對象的構造方法
- 將這個對象的引用賦值給obj
a)、DCL雙重檢查代碼
public class MySingleton {
private static MySingleton INSTANCE;
private MySingleton() {
}
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized (MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
}
}
return INSTANCE;
}
}
b)、位元組碼如下
從位元組碼中可以看到,new MySingleton();
這句代碼對應了17、20、21、24這四行位元組碼(20行是一個引用的拷貝,可以忽略)。
- 首先在17行在記憶體中開闢一塊空間創建一個MySingleton對象。
- 然後在21行調用該對象的構造方法。
- 然後在24行將該對象的引用賦值給靜態變數INSTANCE。
以上是我們期望的執行順序,我們希望每個線程都按照該順序去執行指令(這就是禁止指令重排序)。但是由於電腦為了提高運行效率,會將我們的指令順序進行優化重排(比如上面的順序可能會優化重排為:17、24、21)
指令重排序帶來的問題
- 我們的電腦為了提升效率,會將我們的代碼順序做一些優化,比如在t1線程中的執行順序是 17、24、21,在t2線程中執行的順序是17、21、24 (在單個線程中不管是那種執行順序都不會有問題)。
- 當t1線程獲取到鎖執行對象創建的時候,先執行了24行,將該對象的引用賦值給了靜態變數INSTANCE(此時對象還沒調用構造方法,該對象還不是一個完整的對象)。
- 此時t2線程開始運行了,當t2線程執行到
if (INSTANCE == null)
(第16行代碼)語句的時候,t2線程發現INSTANCE不為空,此時t2線程直接返回INSTANCE對象。但是此時該對象還是一個不完整的對象,在t2線程使用該對象的時候就會出現問題。
所以說指令重排序在單線程中是不會有任何問題的,但是一旦涉及到多線程的情況,那麼指令重排序可能會帶來意想不到的結果。
有序性
那麼既然synchronized不能禁止指令重排序,那麼他保證的有序性是什麼有序呢?
它的本質是讓多個線程在調用synchronized修飾的方法時,由並行(併發)變成串列調用,誰獲得鎖誰執行。
1、代碼示例
t1、t2兩個線程都需要去獲取單例對象,然後調用test方法,並且test方法是加了同步鎖的方法。
public class MySingleton {
private static MySingleton INSTANCE;
private MySingleton() {
}
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized (MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
}
}
return INSTANCE;
}
public static void test(final MySingleton singleton) {
synchronized (MySingleton.class) {
System.out.println(singleton);
}
}
}
測試代碼
public class MySingletonTest {
// 可以看到兩個線程都需要去獲取單例對象,然後調用test方法,並且test方法是加了同步鎖的方法
public static void main(final String[] args) {
new Thread(() -> {
MySingleton instance = MySingleton.getInstance();
MySingleton.test(instance);
}, "t1").start();
new Thread(() -> {
MySingleton instance = MySingleton.getInstance();
MySingleton.test(instance);
}, "t2").start();
}
}
即使是t2線程獲得了未調用構造函數的對象,那麼在t2線程中再去調用MySingleton.test(instance);
方法的時候,也並不會出現任何問題,因為使用了同步鎖,每個一加鎖執行的方法都變成了串列,將併發執行變成了串列,當t2線程獲取到鎖然後執行的時候,t1早已經釋放了鎖,此時instance也已經早就被實例化好了。所以不會出現問題。
所以synchronized保證順序性是指的將併發執行變成了串列,但並不能保證內部指令重排序問題。
來源:blog.csdn.net/Hellowenpan/article/details/117750543
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!