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