指令重排簡述
1、JMM記憶體模型三大特性包括原子性,可見性,有序性。詳細請看https://juejin.im/post/5cb5d419e51d456e500f7d02。
2、指令重排是相對有序性來說的,指在程式執行過程中, 為了效能考慮, 編譯器和CPU可能會對指令重新排序。單執行緒模式下只有一個執行引擎,不存在競爭,所有的操作都是有有序的,不影響最後的執行結果。
3、指令重排只能保證序列(單執行緒)語句執行的一致性。
單例模式
假設我的單列物件是Faith(一個人只有一個信仰),檢視多執行緒下示例的建立次數,即建構函式的呼叫次數。
餓漢模式
示例程式碼
class Faith {
private static Faith myFaith = new Faith();
private Faith(){
System.out.println("Faith.Faith --- 私有構造呼叫了");
}
public static Faith getMyFaith() {
return myFaith;
}
}
public class TestSingleton {
public static void main(String[] args) {
for (int i = 0; i <= 10; i++) {
new Thread(() -> {
Faith.getMyFaith();
},String.valueOf(i)).start();
}
}
}
複製程式碼
控制檯:
Faith.Faith --- 私有構造呼叫了
複製程式碼
- 多條執行緒同時執行時,只建立了一個例項。
- 餓漢模式下,在類載入的時候建立一次例項,不會存在多個執行緒建立多個例項的情況。但在類載入時就自動建立,佔用記憶體。
- 因此重點講懶漢模式,即第一次呼叫獲取實列方法時,才被動建立物件。
懶漢模式
單執行緒懶漢模式
示例程式碼
class Faith {
private static Faith myFaith = null;
private Faith(){
System.out.println(Thread.currentThread().getName()+" --- Faith.Faith --- 私有構造呼叫了");
}
public static Faith getMyFaith() {
if (myFaith == null){
myFaith = new Faith();
}
return myFaith;
}
}
複製程式碼
上面的程式碼是單執行緒下的懶漢模式,但是在併發情況下,當myFaith為空,需new物件時,多個執行緒可能同時進入這個方法。
public class TestSingleton {
public static void main(String[] args) {
for (int i = 0; i <= 10; i++) {
new Thread(() -> {
Faith.getMyFaith();
},String.valueOf(i)).start();
}
}
}
複製程式碼
控制檯:
5 --- Faith.Faith --- 私有構造呼叫了
1 --- Faith.Faith --- 私有構造呼叫了
8 --- Faith.Faith --- 私有構造呼叫了
4 --- Faith.Faith --- 私有構造呼叫了
2 --- Faith.Faith --- 私有構造呼叫了
3 --- Faith.Faith --- 私有構造呼叫了
9 --- Faith.Faith --- 私有構造呼叫了
7 --- Faith.Faith --- 私有構造呼叫了
10 --- Faith.Faith --- 私有構造呼叫了
0 --- Faith.Faith --- 私有構造呼叫了
6 --- Faith.Faith --- 私有構造呼叫了
複製程式碼
可以看到,結果非常糟糕,得到多個不同物件。
多執行緒懶漢模式-synchronized
最直接的方法就是在靜態方法上加synchronized
互斥鎖.
public static synchronized Faith getMyFaith() {
if (myFaith == null){
myFaith = new Faith();
}
return myFaith;
}
複製程式碼
synchronized屬於重量鎖,在高併發情況下,上百條個執行緒都等在靜態方法外,阻塞很大,不推薦。
多執行緒懶漢模式-DCL
DCL(double check lock)雙端檢索機制,在new方法上加同步鎖,但要在加鎖前後進行非空判斷。
class Faith {
private static Faith myFaith = null;
private Faith(){
System.out.println(Thread.currentThread().getName()+" --- Faith.Faith --- 私有構造呼叫了");
}
public static Faith getMyFaith() {
// 第一次判斷,若myFaith例項為空
if (myFaith == null){
// 加同步鎖
synchronized (Faith.class) {
// 第二次判斷,若myFaith例項確實為空,進入構造方法
if (myFaith == null) {
myFaith = new Faith();
}
}
}
return myFaith;
}
}
複製程式碼
public class TestSingleton {
public static void main(String[] args) {
for (int i = 0; i <= 10; i++) {
new Thread(() -> {
Faith.getMyFaith();
},String.valueOf(i)).start();
}
}
}
複製程式碼
控制檯:
0 --- Faith.Faith --- 私有構造呼叫了
複製程式碼
- 可以看到,10條執行緒下,只獲取到一個實列物件,看似是一個相對高效的方法。但在本文一開始,就提到了指令重排。
- 當myFaith為空,進入初始化,當還沒初始化完成時,會有執行緒安全問題。
指令重排分析
myFaith = new Faith();,該方法其實有3步:
1、分配記憶體空間何記憶體地址
memeory = allocate;
複製程式碼
2、初始化物件
myFaith(memory);
複製程式碼
3、將例項指向分配的記憶體地址
myFaith = memory;
複製程式碼
第二步和第三步沒有資料依賴關係,單執行緒下指令重排不影響執行結果,因此編譯器和cpu允許重排優化的行為。
即可能出現第三步先於第二部執行, myFaith = memory; 此時因為已經給即將建立的myFaith分配了記憶體空間,所以myFaith!=null,但物件的初始化還沒有完成,造成執行緒安全問題。
多執行緒懶漢模式-DCL+volatile
JMM保證有序性的重要方法就是引入J.U.C併發包下的volatile
關鍵字,volatile 關鍵字通過新增記憶體屏障的方式來禁止指令重排,即重排序時不能把後面的指令放到記憶體屏障之前。
即原來的DCL單例模式,在例項物件上再加volatile修飾即可。
private static volatile Faith myFaith = null;
複製程式碼