指令重排簡述
1、JMM內(nèi)存模型三大特性包括原子性,可見性,有序性。詳細請看關(guān)于Java內(nèi)存模型的三大特性
2、指令重排是相對有序性來說的,指在程序執(zhí)行過程中, 為了性能考慮, 編譯器和CPU可能會對指令重新排序。單線程模式下只有一個執(zhí)行引擎,不存在競爭,所有的操作都是有有序的,不影響最后的執(zhí)行結(jié)果。
3、指令重排只能保證串行(單線程)語句執(zhí)行的一致性。
單例模式
假設(shè)我的單列對象是Faith(一個人只有一個信仰),查看多線程下示例的創(chuàng)建次數(shù),即構(gòu)造函數(shù)的調(diào)用次數(shù)。
餓漢模式
示例代碼
class Faith {
private static Faith myFaith = new Faith();
private Faith(){
System.out.println("Faith.Faith --- 私有構(gòu)造調(diào)用了");
}
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 --- 私有構(gòu)造調(diào)用了
- 多條線程同時運行時,只創(chuàng)建了一個實例。
- 餓漢模式下,在類加載的時候創(chuàng)建一次實例,不會存在多個線程創(chuàng)建多個實例的情況。但在類加載時就自動創(chuàng)建,占用內(nèi)存。
- 因此重點講懶漢模式,即第一次調(diào)用獲取實列方法時,才被動創(chuàng)建對象。
懶漢模式
單線程懶漢模式
示例代碼
class Faith {
private static Faith myFaith = null;
private Faith(){
System.out.println(Thread.currentThread().getName()+" --- Faith.Faith --- 私有構(gòu)造調(diào)用了");
}
public static Faith getMyFaith() {
if (myFaith == null){
myFaith = new Faith();
}
return myFaith;
}
}
上面的代碼是單線程下的懶漢模式,但是在并發(fā)情況下,當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 --- 私有構(gòu)造調(diào)用了
1 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
8 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
4 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
2 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
3 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
9 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
7 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
10 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
0 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
6 --- Faith.Faith --- 私有構(gòu)造調(diào)用了
可以看到,結(jié)果非常糟糕,得到多個不同對象。
多線程懶漢模式-synchronized
最直接的方法就是在靜態(tài)方法上加synchronized互斥鎖.
public static synchronized Faith getMyFaith() {
if (myFaith == null){
myFaith = new Faith();
}
return myFaith;
}
synchronized屬于重量鎖,在高并發(fā)情況下,上百條個線程都等在靜態(tài)方法外,阻塞很大,不推薦。
多線程懶漢模式-DCL
DCL(double check lock)雙端檢索機制,在new方法上加同步鎖,但要在加鎖前后進行非空判斷。
class Faith {
private static Faith myFaith = null;
private Faith(){
System.out.println(Thread.currentThread().getName()+" --- Faith.Faith --- 私有構(gòu)造調(diào)用了");
}
public static Faith getMyFaith() {
// 第一次判斷,若myFaith實例為空
if (myFaith == null){
// 加同步鎖
synchronized (Faith.class) {
// 第二次判斷,若myFaith實例確實為空,進入構(gòu)造方法
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 --- 私有構(gòu)造調(diào)用了
- 可以看到,10條線程下,只獲取到一個實列對象,看似是一個相對高效的方法。但在本文一開始,就提到了指令重排。
- 當myFaith為空,進入初始化,當還沒初始化完成時,會有線程安全問題。
指令重排分析
myFaith = new Faith();,該方法其實有3步:
1、分配內(nèi)存空間何內(nèi)存地址
memeory = allocate;
2、初始化對象
myFaith(memory);
3、將實例指向分配的內(nèi)存地址
myFaith = memory;
第二步和第三步?jīng)]有數(shù)據(jù)依賴關(guān)系,單線程下指令重排不影響執(zhí)行結(jié)果,因此編譯器和cpu允許重排優(yōu)化的行為。
即可能出現(xiàn)第三步先于第二部執(zhí)行, myFaith = memory; 此時因為已經(jīng)給即將創(chuàng)建的myFaith分配了內(nèi)存空間,所以myFaith!=null,但對象的初始化還沒有完成,造成線程安全問題。
多線程懶漢模式-DCL+volatile
JMM保證有序性的重要方法就是引入J.U.C并發(fā)包下的volatile關(guān)鍵字,volatile 關(guān)鍵字通過添加內(nèi)存屏障的方式來禁止指令重排,即重排序時不能把后面的指令放到內(nèi)存屏障之前。
即原來的DCL單例模式,在實例對象上再加volatile修飾即可。
private static volatile Faith myFaith = null;