Java 併發系列之十三:安全發佈

1. 定義

  • 發佈對象(Publish): 使一個對象可以被當前範圍以外的代碼所使用
  • 對象逸出(Escape): 一種錯誤的發佈。當一個對象尚未構造完成時,就使它被其餘線程所見

1.1 發佈對象

public class UnsafePublish {
    private String[] states = {"a","b","c"};
    public String[] getStates(){
        return states;
    }

    /**
     * 經過new UnsafePublish()發佈了一個UnsafePublish類的實例
     * 經過實例的public方法獲得了私有域states數組的引用
     * 能夠在其餘任何線程裏修改這個數組裏的值
     * 這樣在其餘線程中想使用states數組時,它的值是不徹底肯定的
     * 所以這樣發佈的對象是線程不安全的,由於沒法保證是否有其餘線程對數組裏的值進行了修改
     * @param args
     */
    public static void main(String[] args) {
        UnsafePublish unsafePublish = new UnsafePublish();
        for (String i : unsafePublish.getStates()) {
            System.out.print(i+" ");
        }
        unsafePublish.getStates()[0] = "d";
        System.out.println();
        for (String i : unsafePublish.getStates()) {
            System.out.print(i+" ");
        }
    }
}

 

1.2 對象逸出

@NotThreadSafe
public class Escape {
    private int thisCanBeEscape = 0;
    public Escape(){
        new InnerClass();
    }

    /**
     * 內部類的實例裏面包含了對封裝內容thisCanBeEscape的隱含引用
     * 這樣在對象沒有被正確構造以前,他就會被髮布,有可能有不安全的因素在
     * 一個致使this引用在構造期間逸出的錯誤   是在構造的函數過程當中啓動了一個線程
     * 不管是隱式的啓動仍是顯示地啓動都會形成this引用的逸出,新線程老是會在對象構造完畢
     * 以前就已經看到this引用    因此要再構造函數中使用線程,就不要啓動它而應該專有的start或初始化的方法來統一啓動線程,
     * 能夠採用工廠方法和私有構造函數來完成對象建立和監聽器的註冊等
     *
     *
     * 在對象未完成構造以前   不能夠將其發佈
     */
    private class InnerClass{
        public InnerClass(){
            System.out.println(Escape.this.thisCanBeEscape);
        }
    }

    public static void main(String[] args) {
        new Escape();
    }
}

 

 

2. 問題(引用+狀態,構造函數+正確發佈)

不正確的發佈可變對象致使的兩種錯誤:html

一、發佈線程以外的全部線程均可以看到被髮布對象的過時的值【引用過時】
二、線程看到的被髮布對象的引用是最新的,然而被髮布對象的狀態倒是過時的【狀態過時】編程

正確發佈一個對象遇到的兩個問題:數組

  (1)引用自己要被其餘線程看到;安全

  (2)對象的狀態要被其餘線程看到。多線程

  ps: 在多線程編程中,首要的原則,就是要避免對象的共享,由於若是沒有對象的共享,那麼多線程編寫要輕鬆得多,可是,若是要共享對象,那麼除了可以正確的將構造函數書寫正確外,如何正確的發佈也是一個很重要的問題。函數

  

public class Client {
    public Holder holder;
    
    public void initialize(){
        holder = new Holder(42);//這個代碼不是原子的
    }
}

public class Holder {
    int n;
    public Holder(int n) {
        this.n = n;
    }
    public void assertSanity() {
        if(n != n)
             throw new AssertionError("This statement is false.");
    }
}

/**
    在Client類中,Holder對象被髮布了,可是這是一個不正確的發佈。因爲可見性問題,其餘線程看到的Holder對象將處於不一致的狀態,即便在該對象的構成構函數中已經正確的該構建了不變性條件,這種不正確的發佈致使其餘線程看到還沒有建立完成的對象。主要是Holder對象的建立不是原子性的,可能還未構造完成,其餘線程就開始調用Holder對象。

    因爲沒有使用同步的方法來卻確保Holder對象(包含引用和對象狀態都沒有)對其餘線程可見,所以將Holder成爲未正確發佈。問題不在於Holder自己,而是其沒有正確的發佈。上面沒有正確發佈的可能致使的問題:

    別的線程對於holder字段,可能會看到過期的值,這樣就會    致使空引用,或者是過期的值(即便holder已經被設置了)(引用自己沒有被別的線程看到)
更可怕的是,對於已經更新holder,及時可以看到引用的更新,可是對於對象的狀態,看到的卻多是舊值,對於上面的代碼,可能會拋出AssertionError異常
主要是holder = new Holder(42);這個代碼不是原子性的,可能在構造未完成時,其餘線程就會調用holder對象引用,從而致使不可預測的結果。
*/

 

3. 安全對象的構造過程

不要在構造函數內顯式或者隱式的的公佈this引用。post

(1)在對象構造期間,不要公佈this引用this

  若是要在構造函數中建立內部類,那麼就不能在構造函數中把他發佈了,應該在構造函數外發布,即等構造函數執行完畢,初始化工做已所有完成,再發布內部類。url

public class EventListener { 
    public EventListener(EventSource eventSource) { 
        // do our initialization ... 
        // register ourselves with the event source 
        eventSource.registerListener(this); 
    } 
    public onEvent(Event e) { // handle the event } 
} 
public class RecordingEventListener extends EventListener { 
    private final ArrayList list; 
    public RecordingEventListener(EventSource eventSource) { 
        super(eventSource); 
        list = Collections.synchronizedList(new ArrayList()); 
    } 
    public onEvent(Event e) { 
        list.add(e); 
        super.onEvent(e); 
    } 
    public Event[] getEvents() { 
        return (Event[]) list.toArray(new Event[0]); 
    }  
} 

  

(2)不要隱式地暴露「this」引用spa

public class EventListener2 { 
    public EventListener2(EventSource eventSource) { eventSource.registerListener( 
            new EventListener() { 
                public onEvent(Event e) { 
                    eventReceived(e); 
                } 
     }); 
    } 
    public eventReceived(Event e) { } 
} 
一樣也是子類化問題

(3)不要從構造函數內啓動線程
     a)在構造函數中啓動線程時,構造函數還未執行完畢,不能保證此對象已經徹底構造
     b)若是在啓動的線程中訪問此對象,不能保證訪問到的是徹底構造好的對象

 

3. 安全發佈經常使用模式

要安全的發佈一個對象,對象的引用和對象的狀態必須同時對其餘線程可見。通常一個正確構造的對象(構造函數不發生this逃逸),能夠經過以下方式來正確發佈:

  (1)在靜態初始化函數中初始化一個對象引用

  (2)將一個對象引用保存在volatile類型的域或者是AtomicReference對象中

  (3)將對象的引用保存到某個正確構造對象的final類型的域中。

  (4)將對象的引用保存到一個由鎖保護的域

/**

   不變性: 某個對象在被建立後其狀態就不能被修改,那麼這個對象就稱爲不可變對象,不可變對象必定是線程安全的。不可變對象很簡單。他們只有一種狀態,而且該狀態由構造函數來控制。

  當知足如下條件時,對象纔是不可變的:

    1)對象建立之後其狀態就不能改變;

    2)對象的全部域都是final類型;

    3)對象是正確創造的(在對象建立期間,this引用沒有溢出)。

(1)Java中存在三種對象
   a)不變對象:對象狀態建立後不能再修改,對象的全部域爲final,對象是正確構造的
   b)基本不變對象:不知足不變對象的約束,可是初始化後再也不變化
   c)可變對象:不知足上述不變對象和基本不變對象的約束

(2)安全發佈技術
   a)即確保對象引用和狀態對其餘線程正確可見
   b)方式
      靜態初始化器初始化對象引用
      將引用存儲到volatile域
      將引用存儲到正確建立對象的final域
      將引用存儲到由鎖正確保護的域

(3)三種對象安全發佈方式
   a)不變對象:任何形式機制發佈
   b)基本不變對象:保證安全發佈便可
   c)可變對象:不只要保證安全發佈,並且要確保對象狀態的正確改變(即用鎖或其餘方式,保證對象狀態的正確改變)

  一般,要發佈一個靜態構造的對象,最簡單和最安全的方式是使用靜態初始化器: public static Holder = new Holder(42);

  靜態初始化器由JVM在類的初始化階段執行,因爲JVM內部存在同步機制,因此這種方式初始化對象均可以被安全的發佈。

  對於可變對象,安全的發佈之時確保在發佈當時狀態的可見性,而在隨後的每次對象的訪問時,一樣須要使用同步來確保修改操做的可見性。(狀態可見性+同步)

**/

 

4. 容器安全發佈保證

  在線程安全容器內部同步意味着,在將對象放到某個容器中,好比Vector中,將知足上面的最後一條需求。若是線程A將對象X放到一個線程安全的容器中,隨後線程B讀取這個對象,那麼能夠確保能夠確保B看到A設置的X狀態,即使是這段讀/寫X的應用程序代碼沒有包含顯示的同步。下面容器內提供了安全發佈的保證:

  (1)經過將一個鍵或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,能夠安全將它發佈給任何從這些容器中訪問它的線程。

  (2)經過將某個元素放到Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchroizedList,能夠將該元素安全的發佈到任何從這些容器中訪問該元素的線程。

  (3)經過將元素放到BlockingQueue或者是ConcrrentLinkedQueue中,能夠將該元素安全的發佈到任何從這些訪問隊列中訪問該元素的線程。

  

5. 網址

  1. 安全發佈對象(一)

  2. Java多線程——volatile關鍵字、發佈和逸出

  3. Java多線程——不變性與安全發佈

  4. 第三章 對象的共享(三)

相關文章
相關標籤/搜索