1. 程式人生 > >寫一個安全的Java單例

寫一個安全的Java單例

work 這樣的 演示 try 解決 string 什麽 機制 print

  單例模式可能是我們平常工作中最常用的一種設計模式了。單例模式解決的問題也很常見,即如何創建一個唯一的對象。但想安全的創建它其實並不容易,還需要一些思考和對JVM的了解。

  1.首先,課本上告訴我,單例這麽寫

  

 1 public class Singleton {
 2 
 3     private static Singleton instance;
 4 
 5     private Singleton() {
 6     }
 7 
 8     public static Singleton getInstance() {
 9         if (instance == null
) { 10 instance = new Singleton(); 11 } 12 return instance; 13 } 14 }

  這段代碼最大的問題就是它並不是線程安全的。即在多線程情況下可能new 出多個對象。試想有兩個線程同時執行到了第9行,由於沒有鎖機制,那麽兩個線程都會進入,就會new出多個對象。

 1 public static CountDownLatch latch = new CountDownLatch(2);
 2 
 3     public static void main(String[] args) {
4 for (int i = 0; i < 2; i++) { 5 new Thread(new Runnable() { 6 7 @Override 8 public void run() { 9 latch.countDown(); 10 try { 11 latch.await(); 12 } catch
(InterruptedException e) { 13 // TODO Auto-generated catch block 14 e.printStackTrace(); 15 } 16 System.out.println(Singleton.getInstance()); 17 } 18 }).start(); 19 } 20 }

  我用上面代碼來演示 第一種單例寫法的結果。最後會調用Object的toString方法來打印Singleton對象的hashcode

  結果如下

  

第一次結果:
com.deng.pp.Singleton@33bbe97
com.deng.pp.Singleton@11989480

第二次結果:
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@5e70125b

第三次結果:
com.deng.pp.Singleton@5e70125b
com.deng.pp.Singleton@1c0956b9

第四次結果:
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9

第五次結果:
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9

  可以看出,單例代碼1確實會存在new 出多個對象的情況。

  將單例代碼1的getInstance方法 改成如下,對getInstance方法加synchronized 關鍵字

  

public static synchronized Singleton getInstance() {
  if (instance == null) {
    instance = new Singleton();
  }
  return instance;
}

  下面是5次測試結果

  

第一次:
com.deng.pp.Singleton@33bbe97
com.deng.pp.Singleton@33bbe97
第二次
com.deng.pp.Singleton@12ceba90
com.deng.pp.Singleton@12ceba90
第三次
com.deng.pp.Singleton@5e70125b
com.deng.pp.Singleton@5e70125b
第四次
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9
第五次
com.deng.pp.Singleton@1c0956b9
com.deng.pp.Singleton@1c0956b9

  可以確定synchronized確實起了作用。這麽做是可以work的,執行結果也沒有什麽錯誤。但它有一個最大問題是效率問題。每個線程調用getInstance方法是都要去判斷是否有其他線程在執行這個方法,即使instance已經存在也需要去判斷是否有線程在方法裏面。如果有,就要在外邊等。而實際上只需要在new 對象之前等就可以了。根據這個就有了下面的方法:雙重檢查鎖

  

 1 public static Singleton getInstance() {
 2         if (instance == null) {
 3             synchronized (Singleton.class) {
 4                 if (instance == null) {
 5                     instance = new Singleton();
 6                 }
 7             }
 8         }
 9         return instance;
10     }

  來分析一下,假設兩個線程到達getInstance方法,線程1先獲得了鎖,進入初始化方法。線程2因未獲得鎖在外邊等待,線程1出去後,線程2進入同步塊,instance不是null,return,完美。

  但是結果可能並不是這樣,因為 對象的new操作並不是 原子 的。JVM new 對象的過程大致如下

  1.在堆上分配一塊內存空間 2.實例化類放入1分配的內存空間 3.把引用賦給instance

  如果按照123的順序,上面那段代碼就沒有問題。但JVM中存在指令重排,即編譯器對代碼進行優化,改變不相互依賴的代碼的執行順序。上述1,2,3中第三步並不依賴於第二步,即可能存在132這樣的順序。

  那麽這種順序下,線程1執行到3。線程2進入方法,此時由於instance已被賦值,所以不為null。直接return,此時return的對象是不正確的,因為線程1還沒有將對象完全初始化完。

  (很抱歉,在我的環境下並沒有重現這種問題,如果有其他的可以測試出這種問題的方法,望不吝賜教。)

  解決辦法是將instance字段改成

    private volatile static Singleton instance;

  volatile 關鍵字會禁止指令重排序,從而保證了單例正確性。

  下面的方法也可以實現單例,因為SINGLETON為static的所以在類加載時就會初始化,final保證了只會賦一遍值。項目較小時可以用,很方便,類很多的時候如果都上來就加載可能就很浪費資源了。

public static final Singleton SINGLETON = new Singleton();

  Effective Java作者推薦了一種更好更安全的寫法。

public class Singleton {

    private Singleton() {
    }

    public enum Instance{
        INSTANCE;
        
        private Singleton singleton;
        
        Instance() {
            singleton = new Singleton();
        }
        
        public Singleton getInstance(){
            return singleton;
        }
    }
}

  最近才開始寫博客,才疏學淺,如文中有任何錯誤請留言交流。謝謝~

寫一個安全的Java單例