對象部分初始化被稱爲 Partially initialized objects / Partially constructed objects / Incompletely initialized objectshtml
這三種不一樣的說法描述的是同一種狀況,即指令重排序(reorder)致使未徹底初始化的對象被使用,這會致使某些錯誤的發生。java
文章純原創,轉載請代表地址設計模式
要理解對象部分初始化,那就要先理解對象初始化。多線程
package Singleton; public class NewObject { public static void main(String[] args) { NewObject newObject = new NewObject(); } }
上面是一個很是簡單的新建對象代碼,newObject字段指向堆中新創建的對象,將上面代碼反編譯成字節碼。oracle
0 new #2 <Singleton/NewObject> 3 dup 4 invokespecial #3 <Singleton/NewObject.<init>> 7 astore_1 8 return
根據Oracle官方文檔描述,第0行(以行前標記爲準) 的new
指令進行了以下操做dom
Memory for a new instance of that class is allocated from the garbage-collected heap, and the instance variables of the new object are initialized to their default initial values (§2.3, §2.4). The objectref, a
reference
to the instance, is pushed onto the operand stack.jvm
翻譯一下就是,該指令爲指定類的實例在堆中分配了內存空間,而且將這個新對象的實例變量進行了默認初始化,即 int
類型爲 0, boolean
類型爲 false。而且該指令還將一個指向該實例的引用推入操做數棧中。ide
而dup
複製一份操做數棧頂的值,而且推入棧中 。函數
這個指令比較複雜,此處只須要知道該指令在此處調用了對象的初始化函數 NewObject.<init>
,對象初始化會按照靜態變量、靜態初始化塊->變量、初始化塊->構造器等順序進行初始化,這個不是關鍵,關鍵是初始化在此時進行。該指令結束後對象會被正確的初始化。學習
該指令將操做數棧頂的值儲存到局部變量表中,astore_1
在此處表明的就是將值儲存到變量newObjec
t中。
若是變量不是聲明在方法中,而是聲明在類中,那指令會變爲putfield
。不管變量聲明在何處,使用哪一個指令,目的是爲了將操做數棧頂的值儲存到它該去的地方。
初始化的過程看起來沒有任何問題,按照123的順序執行的話在使用對象引用時對象必定是初始化完成的,可是爲了效率,當今的CPU是」流水線「執行指令,即指令順序輸入,亂序執行,CPU在確保最終結果的前提下會按照最高效率的方式執行指令,而不是順序的執行。
在對象初始化的過程當中,CPU極可能的執行順序是132,即 new
astore
invokespecial
。
若是是在單線程的狀況下,132的執行順序不會形成什麼問題,由於CPU會保證不在invokespecial
完成前使用對象。
可是在多線程的狀況下就不同了,亂序執行會致使線程A在對象初始化完成前就將引用X指向了堆中的對象,這個引用X是共享資源,其餘線程也能看的到這個變量。線程B並不知道線程A中發生了什麼,當線程B須要使用引用X的時候會出現如下三種狀況
對象部分初始化的問題最開始是在學習單例設計模式、雙重檢查鎖(Double-check-lock)的過程當中瞭解到的,DCL因爲指令重排序,不在對象上加volatile
關鍵字就會致使對象部分初始化問題。原理問題在國內外各類博客和論壇上都有描述,也都大同小異。
但困擾個人關鍵在於沒有找到能給出DCL不加volatile會出問題的代碼,換句話說,你們談的都是理論,沒有博客/文章/回答可以用代碼說明這個問題確實存在。
根據維基百科的描述,這個問題是很是難以再現的。
Depending on the compiler, the interleaving of threads by the scheduler and the nature of other concurrent system activity, failures resulting from an incorrect implementation of double-checked locking may only occur intermittently. Reproducing the failures can be difficult.
在我嘗試親手復現錯誤的代碼時,我發現若是要把測試放在單例類中,則一次運行時只能對對象進行一次初始化,其餘線程只有在這一次初始化的間隙中有機會調用「不正確」的對象,在這種狀況下我可能手動把程序跑上三天三夜都沒辦法復現一次這個問題。
因而換了一個思路,並不須要在DCL的單例模式中證實這個問題,只要能證實對象部分初始化問題存在便可。
mport java.util.Random; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; class PartiallyInitializedObject{ static long counter; // final field will avoid partiallyInitializedObject // final long id = counter++; public int n; public PartiallyInitializedObject(int n){ this.n = n; } } class Builder{ public int createNumber = 0; public AtomicInteger getNumber = new AtomicInteger(0); Random rand = new Random(47); //private volatile PartiallyInitializedObject partiallyInitializedObject; private PartiallyInitializedObject partiallyInitializedObject; public PartiallyInitializedObject get(){ getNumber.incrementAndGet(); return partiallyInitializedObject; } public void initialize(){ partiallyInitializedObject = new PartiallyInitializedObject(rand.nextInt(20)+5); createNumber++; } } public class PartiallyInitialized { static class Initialize implements Runnable{ Builder builder; public Initialize(Builder builder){ this.builder = builder; } @Override public void run() { while(!Thread.interrupted()){ builder.initialize(); } } } static class GetObject implements Runnable{ static int count =0; final int id = count++; CyclicBarrier cyclicBarrier; Builder builder; public GetObject(CyclicBarrier c, Builder builder){ cyclicBarrier = c; this.builder = builder; } @Override public void run() { while (!Thread.interrupted()) { PartiallyInitializedObject p = builder.get(); if (p.n == 0) { System.out.println("Thread " + id +" Find Partially Initialized Object " + p.n); Thread.currentThread().interrupt(); } } try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println("Thread " + id +" Interrupted"); } } public static void main(String[] args) throws BrokenBarrierException, InterruptedException{ // first initialize(), second get() // 1 initialize(), 9 get() Builder builder = new Builder(); CyclicBarrier cyclicBarrier = new CyclicBarrier(10); ExecutorService exec = Executors.newFixedThreadPool(10); exec.execute(new Initialize(builder)); for(int i=0; i<9; i++){ exec.execute(new GetObject(cyclicBarrier, builder)); } // exec.execute(new Initialize(builder)); try { cyclicBarrier.await(3, TimeUnit.SECONDS); } catch (TimeoutException e) { System.out.println("No Partially Initialized Object Found"); } exec.shutdownNow(); System.out.println("Builder create "+builder.createNumber +" Object And Try to get "+ builder.getNumber.get()+ " times"); } }
Builder
類中的變量partiallyInitializedObject
不使用volatile
修飾時輸出以下
Thread 5 Find Partially Initialized Object 13 Thread 3 Find Partially Initialized Object 23 Thread 0 Find Partially Initialized Object 6 Thread 1 Find Partially Initialized Object 10 Thread 2 Find Partially Initialized Object 11 Thread 8 Find Partially Initialized Object 23 Thread 4 Find Partially Initialized Object 14 Thread 6 Find Partially Initialized Object 6 Thread 7 Find Partially Initialized Object 24 Thread 7 Interrupted Thread 5 Interrupted Thread 3 Interrupted Thread 8 Interrupted Thread 0 Interrupted Thread 6 Interrupted Thread 4 Interrupted Thread 2 Interrupted Thread 1 Interrupted Builder create 46736 Object And Try to get 231239 times
Builder
類中的變量partiallyInitializedObject
使用volatile
修飾時輸出以下
No Partially Initialized Object Found Builder create 7661170 Object And Try to get 72479637 times Thread 3 Interrupted Thread 7 Interrupted Thread 0 Interrupted Thread 6 Interrupted Thread 1 Interrupted Thread 8 Interrupted Thread 5 Interrupted Thread 2 Interrupted Thread 4 Interrupted java.util.concurrent.BrokenBarrierException at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207) at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362) at Singleton.PartiallyInitialized$GetObject.run(PartiallyInitialized.java:66) ......
代碼中在線程池在執行調用GetObject
線程以前先執行Initialize
的線程,若是把exec.execute(new Initialize(builder));
放到GetObject
的線程後面,那就會出現以前說的三種狀況中的第一種:GetObject
得到的引用爲空。
觀察代碼和輸出,在GetObject
線程中,只有當對象PartiallyInitializedObject.n
的值爲0時纔會進行輸出而且打斷當前線程,而在Builder
的initialize()
中能很明顯的看到,對象的n值是大於等於5而且小於25,即永遠不可能爲0。但輸出的結果卻證實了GetObject
線程在某些時刻確實能獲得爲0的n值。代碼剩餘的細節這裏就再也不贅述。
到這一步就可以說明確實存在指令重排序而致使的對象部分初始化問題,因爲synchronized
和volatile
保證可見性和有序性的原理並不相同,因此在DCL單例模式這種特殊的狀況下,synchronized
也不能很好的確保正確。固然,因爲種種緣由,DCL單例模式已經基本被棄用了,這篇文章只作一些相關的探討。
https://stackoverflow.com/questions/7855700/why-is-volatile-used-in-double-checked-locking
https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.new