【轉】深刻淺出單實例SINGLETON設計模式

單例模式理解起來應該不難,可是若是是在多線程下應該如何安全地實現單例模式呢?,看到一篇挺好的文章,順手轉過來,待往後細細回味。html

原文出處:深刻淺出單實例SINGLETON設計模式shell

 


 

Singleton的教學版本

這裏,我將直接給出一個Singleton的簡單實現,由於我相信你已經有這方面的一些基礎了。咱們姑且把這個版本叫作1.0版設計模式

 

 1 // version 1.0
 2 public class Singleton {  3     private static Singleton singleton = null;  4     private Singleton() { }  5     public static Singleton getInstance() {  6         if (singleton== null) {  7             singleton= new Singleton();  8  }  9         return singleton; 10  } 11 }

 

在上面的實例中,我想說明下面幾個Singleton的特色:(下面這些東西多是盡人皆知的,沒有什麼新鮮的)安全

  1. 私有(private)的構造函數,代表這個類是不可能造成實例了。這主要是怕這個類會有多個實例。
  2. 即然這個類是不可能造成實例,那麼,咱們須要一個靜態的方式讓其造成實例:getInstance()。注意這個方法是在new本身,由於其能夠訪問私有的構造函數,因此他是能夠保證明例被建立出來的。
  3. 在getInstance()中,先作判斷是否已造成實例,若是已造成則直接返回,不然建立實例。
  4. 所造成的實例保存在本身類中的私有成員中。
  5. 咱們取實例時,只須要使用Singleton.getInstance()就好了。

固然,若是你以爲知道了上面這些事情後就學成了,那得給你當頭棒喝一下了,事情遠遠沒有那麼簡單。多線程

 

Singleton的實際版本

上面的這個程序存在比較嚴重的問題,由於是全局性的實例,因此,在多線程狀況下,全部的全局共享的東西都會變得很是的危險,這個也同樣,在多線程狀況下,若是多個線程同時調用getInstance()的話,那麼,可能會有多個進程同時經過 (singleton== null)的條件檢查,因而,多個實例就建立出來,而且極可能形成內存泄露問題。嗯,熟悉多線程的你必定會說——「咱們須要線程互斥或同步」,沒錯,咱們須要這個事情,因而咱們的Singleton升級成1.1版,以下所示:

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

 

嗯,使用了Java的synchronized方法,看起來不錯哦。應該沒有問題了吧?!錯!這仍是有問題!爲何呢?前面已經說過,若是有多個線程同時經過(singleton== null)的條件檢查(由於他們並行運行),雖然咱們的synchronized方法會幫助咱們同步全部的線程,讓咱們並行線程變成串行的一個一個去new,那不仍是同樣的嗎?一樣會出現不少實例。嗯,確實如此!看來,還得把那個判斷(singleton== null)條件也同步起來。因而,咱們的Singleton再次升級成1.2版本,以下所示:函數

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

 

不錯不錯,看似很不錯了。在多線程下應該沒有什麼問題了,不是嗎?的確是這樣的,1.2版的Singleton在多線程下的確沒有問題了,由於咱們同步了全部的線程。只不過嘛……,什麼?!還不行?!是的,仍是有點小問題,咱們原本只是想讓new這個操做並行就能夠了,如今,只要是進入getInstance()的線程都得同步啊,注意,建立對象的動做只有一次,後面的動做全是讀取那個成員變量,這些讀取的動做不須要線程同步啊。這樣的做法感受很是極端啊,爲了一個初始化的建立動做,竟然讓咱們達上了全部的讀操做,嚴重影響後續的性能啊!性能

還得改!嗯,看來,在線程同步前還得加一個(singleton== null)的條件判斷,若是對象已經建立了,那麼就不須要線程的同步了。OK,下面是1.3版的Singleton。優化

// version 1.3
public class Singleton { private static Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (singleton== null) { synchronized (Singleton.class) { if (singleton== null) { singleton= new Singleton(); } } } return singleton; } }

 

感受代碼開始變得有點羅嗦和複雜了,不過,這多是最不錯的一個版本了,這個版本又叫「雙重檢查」Double-Check。下面是說明:spa

  1. 第一個條件是說,若是實例建立了,那就不須要同步了,直接返回就行了。
  2. 否則,咱們就開始同步線程。
  3. 第二個條件是說,若是被同步的線程中,有一個線程建立了對象,那麼別的線程就不用再建立了。

至關不錯啊,乾得很是漂亮!請你們爲咱們的1.3版起立鼓掌!線程

可是,若是你認爲這個版本大攻告成,你就錯了。

主要在於singleton = new Singleton()這句,這並不是是一個原子操做,事實上在 JVM 中這句話大概作了下面 3 件事情。

  1. 給 singleton 分配內存
  2. 調用 Singleton 的構造函數來初始化成員變量,造成實例
  3. 將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)

可是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序多是 1-2-3 也多是 1-3-2。若是是後者,則在 3 執行完畢、2 未執行以前,被線程二搶佔了,這時 instance 已是非 null 了(但卻沒有初始化),因此線程二會直接返回 instance,而後使用,而後瓜熟蒂落地報錯。

對此,咱們只須要把singleton聲明成 volatile 就能夠了。下面是1.4版:

 

// version 1.4
public class Singleton { private volatile static Singleton singleton = null; private Singleton() { } public static Singleton getInstance() { if (singleton== null) { synchronized (Singleton.class) { if (singleton== null) { singleton= new Singleton(); } } } return singleton; } }

使用 volatile 有兩個功用:

1)這個變量不會在多個線程中存在複本,直接從內存讀取。

2)這個關鍵字會禁止指令重排序優化。也就是說,在 volatile 變量的賦值操做後面會有一個內存屏障(生成的彙編代碼上),讀操做不會被重排序到內存屏障以前。

可是,這個事情僅在Java 1.5版後有用,1.5版以前用這個變量也有問題,由於老版本的Java的內存模型是有缺陷的。

 

原文出處:https://coolshell.cn/articles/265.html

相關文章
相關標籤/搜索