對象部分初始化:原理以及驗證代碼(雙重檢查鎖與volatile相關)

對象部分初始化:原理以及驗證代碼(雙重檢查鎖與volatile相關)

對象部分初始化被稱爲 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

閱讀字節碼

1. new

根據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複製一份操做數棧頂的值,而且推入棧中 。函數

2. invokespecial

這個指令比較複雜,此處只須要知道該指令在此處調用了對象的初始化函數 NewObject.<init>,對象初始化會按照靜態變量、靜態初始化塊->變量、初始化塊->構造器等順序進行初始化,這個不是關鍵,關鍵是初始化在此時進行。該指令結束後對象會被正確的初始化。學習

3. astore

該指令將操做數棧頂的值儲存到局部變量表中,astore_1在此處表明的就是將值儲存到變量newObject中。

若是變量不是聲明在方法中,而是聲明在類中,那指令會變爲putfield 。不管變量聲明在何處,使用哪一個指令,目的是爲了將操做數棧頂的值儲存到它該去的地方。

指令重排下的對象初始化

初始化的過程看起來沒有任何問題,按照123的順序執行的話在使用對象引用時對象必定是初始化完成的,可是爲了效率,當今的CPU是」流水線「執行指令,即指令順序輸入,亂序執行,CPU在確保最終結果的前提下會按照最高效率的方式執行指令,而不是順序的執行。

在對象初始化的過程當中,CPU極可能的執行順序是132,即 new astore invokespecial

若是是在單線程的狀況下,132的執行順序不會形成什麼問題,由於CPU會保證不在invokespecial完成前使用對象。

可是在多線程的狀況下就不同了,亂序執行會致使線程A在對象初始化完成前就將引用X指向了堆中的對象,這個引用X是共享資源,其餘線程也能看的到這個變量。線程B並不知道線程A中發生了什麼,當線程B須要使用引用X的時候會出現如下三種狀況

  1. 線程A還未將引用X指向對象,線程B得到的X是null;
  2. 初始化完成,線程B使用的對象是正確的對象;
  3. 引用X指向了堆中的對象,可是線程A中進行的初始化未完成,線程B使用的對象是部分初始化的對象。

Show me the code

對象部分初始化的問題最開始是在學習單例設計模式、雙重檢查鎖(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的單例模式中證實這個問題,只要能證實對象部分初始化問題存在便可。

代碼設計思路:

  1. 亂序重排發生在對象初始化中,須要有一個線程儘量多的進行類的初始化,好讓其餘線程能儘可能捕捉到問題(static class Initialize)
  2. 須要許多個線程不斷的調用被初始化的類,而且判斷這個類是否有被正確初始化(static class GetObject)
  3. 存在一個類做爲被初始化的對象(class PartiallyInitializedObject)
  4. 存在一個類持有上面對象的引用,線程經過這個類進行對象初始化而且給引用賦值,也經過這個類獲取到引用(class Builder)

代碼

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時纔會進行輸出而且打斷當前線程,而在Builderinitialize()中能很明顯的看到,對象的n值是大於等於5而且小於25,即永遠不可能爲0。但輸出的結果卻證實了GetObject線程在某些時刻確實能獲得爲0的n值。代碼剩餘的細節這裏就再也不贅述。

到這一步就可以說明確實存在指令重排序而致使的對象部分初始化問題,因爲synchronizedvolatile保證可見性和有序性的原理並不相同,因此在DCL單例模式這種特殊的狀況下,synchronized也不能很好的確保正確。固然,因爲種種緣由,DCL單例模式已經基本被棄用了,這篇文章只作一些相關的探討。

參考

https://wiki.sei.cmu.edu/confluence/display/java/TSM03-J.+Do+not+publish+partially+initialized+objects

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

相關文章
相關標籤/搜索