1. 程式人生 > >高併發第五彈:安全釋出物件及單例模式

高併發第五彈:安全釋出物件及單例模式

要正確的釋出一個物件首先要解決3個問題: 1.釋出的物件只需要被它需要被看見的執行緒或其它物件看見 2.避免逸出問題 3.避免其它執行緒拿到未初始化完全的物件

什麼是釋出

  釋出一個物件是指,使物件能夠在當前作用域之外的程式碼中使用。比如,將建立的物件儲存到容器中,也可能通過某個方法返回物件的引用,或者將引用傳遞到其他類的方法中。

  在我們的日常開發中,我們經常要釋出一些物件,比如通過類的非私有方法返回物件的引用,或者通過公有靜態變數釋出物件。

什麼是逸出

逸出是指某個不應該釋出的物件被髮布,被其他執行緒或物件看見。也就是一種錯誤的釋出。當一個物件還沒有構造完成時,就使它被其他執行緒所見。

程式碼演示

不安全釋出物件

1.如果在編碼中不希望一個變數被其它使用,就不要通過方法或者容器讓物件逸出

private User user;//類的非私有方法,返回私有物件的引用
public User getUser(){
   return this.user;
}//如果本意不想讓外部獲取user,那麼這個方法就會讓物件逸出
pubic Map getUsers(){
   map.put("u1",user);
   return map;
}//不經意釋出包含user的map,在實際情況中可能

分析:

  這個程式碼通過public訪問級別釋出了類的域,在類的任何外部的執行緒都可以訪問這些域

  我們無法保證其他執行緒會不會修改這個域,從而使私有域內的值錯誤(上述程式碼中就對私有域進行了修改)

物件逸出

  1。在建構函式中啟動一個執行緒

  2. 在建構函式中釋出一個內部的類例項 隱式的使this逸出問題在於考慮到物件未初始化完全,使得其它物件或現程未取得完整初始化的物件而發生未知錯誤。

//摘自於Java 併發程式設計實戰
public class ThisEscape {  
    private String name = null;  
    public ThisEscape(EventSource source) {  
        source.registerListener(
new EventListener() { /*在建構函式註冊監聽器,監聽器屬於另一個執行緒*/ public void onEvent(Event event) { doSomething(event);//等效於this.doSomething(event),this就是在這裡逸出的 } }); name = "TEST"; } protected void doSomething(Event event) { System.out.println(name.toString()); } }

// 看一個更清晰的

public class Escape {

    private Integer thisCanBeEscape = 0;

    public Escape () {
        new InnerClass();
        thisCanBeEscape = null;
    }

    //內部類構造方法呼叫外部類的私有域
    private class InnerClass {

        public InnerClass() {
            log.info("{}", Escape.this.thisCanBeEscape);
        }
    }

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

分析:

    • 這個內部類的例項裡面包含了對封裝例項的私有域物件的引用,在物件沒有被正確構造完成之前就會被髮布,有可能有不安全的因素在裡面,會導致this引用在構造期間溢位的錯誤。
    • 上述程式碼在函式構造過程中啟動了一個執行緒。無論是隱式的啟動還是顯式的啟動,都會造成這個this引用的溢位。新執行緒總會在所屬物件構造完畢之前就已經看到它了。
    • 因此要在建構函式中建立執行緒,那麼不要啟動它,而是應該採用一個專有的start或者初始化的方法統一啟動執行緒
    • 這裡其實我們可以採用工廠方法和私有建構函式來完成物件建立和監聽器的註冊等等,這樣才可以避免錯誤
    • ——————————————————————————————————————————————————-
    • 如果不正確的釋出物件會導致兩種錯誤: (1)釋出執行緒意外的任何執行緒都可以看到被髮布物件的過期的值 (2)執行緒看到的被髮佈線程的引用是最新的,然而被髮布物件的狀態卻是過期的

安全釋出物件示例(多種單例模式演示)

如何安全釋出物件?共有四種方法

  • 1、在靜態初始化函式中初始化一個物件引用
  • 2、將物件的引用儲存到volatile型別域或者AtomicReference物件中
  • 3、將物件的引用儲存到某個正確構造物件的final型別域中
  • 4、將物件的引用儲存到一個由鎖保護的域中

下面我們用各種單例模式來演示其中的幾種方法

1、懶漢式(最簡式)
public class SingletonExample {
    //私有建構函式
    private SingletonExample(){
    }

    //單例物件
    private static SingletonExample instance = null;

    //靜態工廠方法
    public static SingletonExample getInstance(){
        if(instance==null){
            return new SingletonExample();
        }
        return instance;
    }
}

分析:1、在多執行緒環境下,當兩個執行緒同時訪問這個方法,同時制定到instance==null的判斷。都判斷為null,接下來同時執行new操作。這樣類的建構函式被執行了兩次。一旦建構函式中涉及到某些資源的處理,那麼就會發生錯誤。所以說最簡式是執行緒不安全的

2、懶漢式(synchronized)
在類的靜態方法上使用synchronized修飾
 public static synchronized SingletonExample getInstance()

分析: 1、使用synchronized修飾靜態方法後,保證了方法的執行緒安全性,同一時間只有一個執行緒訪問該方法 2、有缺陷:會造成效能損耗

3、雙重同步鎖模式【先入坑再出坑】
public class SingletonExample {
    // 私有建構函式
    private SingletonExample() {
    }
    // 單例物件
    private static SingletonExample instance = null;
    // 靜態的工廠方法
    public static SingletonExample getInstance() {
        if (instance == null) { // 雙重檢測機制
            synchronized (SingletonExample.class) { // 同步鎖
                if (instance == null) {
                    instance = new SingletonExample();
                }
            }
        }
        return instance;
    }
}

(入坑)分析: 1、我們將上面的第二個例子(懶漢式(synchronized))進行了改進,由synchronized修飾方法改為先判斷後,再鎖定整個類,再加上雙重的檢測機制,保證了最大程度上的避免耗損效能。 2、這個方法是執行緒不安全的,可能大家會想在多執行緒情況下,只要有一個執行緒對類進行了上鎖,那麼無論如何其他執行緒也不會執行到new的操作上。接下來我們分析一下執行緒不安全的原因:

這裡有一個知識點:CPU指令相關 在上述程式碼中,執行new操作的時候,CPU一共進行了三次指令 (1)memory = allocate() 分配物件的記憶體空間 (2)ctorInstance() 初始化物件 (3)instance = memory 設定instance指向剛分配的記憶體

在程式執行過程中,CPU為提高運算速度會做出違背程式碼原有順序的優化。我們稱之為亂序執行優化或者說是指令重排。 那麼上面知識點中的三步指令極有可能被優化為(1)(3)(2)的順序。當我們有兩個執行緒A與B,A執行緒遵從132的順序,經過了兩此instance的空值判斷後,執行了new操作,並且cpu在某一瞬間剛結束指令(3),並且還沒有執行指令(2)。而在此時執行緒B恰巧在進行第一次的instance空值判斷,由於執行緒A執行完(3)指令,為instance分配了記憶體,執行緒B判斷instance不為空,直接執行return,返回了instance,這樣就出現了錯誤。 這裡寫圖片描述

(出坑)解決辦法:

在物件宣告時使用volatile關鍵字修飾,阻止CPU的指令重排。
private volatile static SingletonExample instance = null;
4、餓漢式(最簡式)
public class SingletonExample {
    // 私有建構函式
    private SingletonExample() {

    }
    // 單例物件
    private static SingletonExample instance = new SingletonExample();

    // 靜態的工廠方法
    public static SingletonExample getInstance() {
        return instance;
    }
}

分析: 1、餓漢模式由於單例例項是在類裝載的時候進行建立,因此只會被執行一次,所以它是執行緒安全的。 2、該方法存在缺陷:如果建構函式中有著大量的事情操作要做,那麼類的裝載時間會很長,影響效能。如果只是做的類的構造,卻沒有引用,那麼會造成資源浪費 3、餓漢模式適用場景為:(1)私有建構函式在實現的時候沒有太多的處理(2)這個類在例項化後肯定會被使用

5、餓漢式(靜態塊初始化)
  • 1
    public class SingletonExample {
        // 私有建構函式
        private SingletonExample() {
        }
        // 單例物件
        private static SingletonExample instance = null;
        static {
            instance = new SingletonExample();
        }
        // 靜態的工廠方法
        public static SingletonExample getInstance() {
            return instance;
        }
        public static void main(String[] args) {
            System.out.println(getInstance().hashCode());
            System.out.println(getInstance().hashCode());
        }
    }

分析: 1、除了使用靜態域直接初始化單例物件,還可以用靜態塊初始化單例物件。 2、值得注意的一點是,靜態域與靜態塊的順序一定不要反,在寫靜態域和靜態方法的時候,一定要注意順序,不同的靜態程式碼塊是按照順序執行的,它跟我們正常定義的靜態方法和普通方法是不一樣的。

6、列舉式
public class SingletonExample {

    private SingletonExample() {
    }

    public static SingletonExample getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;
        private SingletonExample singleton;

        Singleton() {
            singleton = new SingletonExample();
        }

        public SingletonExample getInstance() {
            return singleton;
        }
    }
}
  • 由於列舉類的特殊性,列舉類的建構函式Singleton方法只會被例項化一次,且是這個類被呼叫之前。這個是JVM保證的。
  • 對比懶漢與餓漢模式,它的優勢很明顯。

感謝