單例模式可以說是設計模式中最簡單的設計模式之一了。顧名思義,單例模式指的是一個類只提供一個固定的單個實例,大家共用該實例。
單例模式代碼實現步驟:
1、私有化類的構造方法
2、提供私有靜態實例變量
3、提供公共靜態方法使其返回私有實例變量
基礎的實現代碼如下:
1 public class Singleton { 2 3 //私有化靜態實例變量 4 private static Singleton instance; 5 6 //私有化構造方法,保證其他類無法創建該類的新對象 7 private Singleton() { 8 } 9 10 public static Singleton getInstance() { 11 //判斷實例是否存在,若不存在則創建一個新對象並返回 12 if (instance == null) { 13 //創建對象時,打印一下 14 System.out.println("創建了一個實例"); 15 instance = new Singleton(); 16 } 17 return instance; 18 } 19 20 }
上述代碼乍一看貌似沒有什麼問題,但是當處於多線程情況下,問題就會暴露出來。接下來編寫main方法進行測試:
1 public class Singleton { 2 3 //私有化靜態實例變量 4 private static Singleton instance; 5 6 //私有化構造方法,保證其他類無法創建該類的新對象 7 private Singleton() { 8 } 9 10 public static Singleton getInstance() { 11 //判斷實例是否存在,若不存在則創建一個新對象並返回 12 if (instance == null) { 13 //創建對象時,打印一下 14 System.out.println("創建了一個實例"); 15 instance = new Singleton(); 16 } 17 return instance; 18 } 19 20 public static void main(String[] args) { 21 //開啓10個線程,調用獲取實例方法 22 for (int i = 0; i < 10; i++) { 23 new Thread(() -> { 24 Singleton.getInstance(); 25 }).start(); 26 } 27 } 28 }
程序運行結果如下(每次程序運行結果都可能不同):
結果很明顯,在多線程情況下,使用上述代碼創建單例實例時,會造成創建多個實例的問題。原因就在於上述代碼並沒有加鎖,導致代碼第12行,對instance判斷爲空時,可能存在多個線程同時執行到這一步,各個線程都認爲實例沒有被創建,於是又各自執行了初始化創建實例的代碼(代碼第15行)。
問題的原因找到了,那麼就好解決了,以下提供幾種解決方法。
解決方法一:使用synchronized關鍵字修飾getInstance方法
使用synchronized關鍵字修飾getInstance方法,將其變爲同步方法,即意味着該方法在某個線程首次進入該方法執行完成之後,其他線程才能進入該方法,而當其他線程再次進入該方法時,instance實例已經被首次進入該方法的線程初始化好了,於是就避免了產生重複創建對象的問題。示例代碼如下:
1 public class Singleton { 2 3 //私有化靜態實例變量 4 private static Singleton instance; 5 6 //私有化構造方法,保證其他類無法創建該類的新對象 7 private Singleton() { 8 } 9 10 //添加了synchronized修飾該方法,使其變成同步方法 11 public static synchronized Singleton getInstance() { 12 //判斷實例是否存在,若不存在則創建一個新對象並返回 13 if (instance == null) { 14 //創建對象時,打印一下 15 System.out.println("創建了一個實例"); 16 instance = new Singleton(); 17 } 18 return instance; 19 } 20 21 public static void main(String[] args) { 22 //開啓10個線程,調用獲取實例方法 23 for (int i = 0; i < 10; i++) { 24 new Thread(() -> { 25 Singleton.getInstance(); 26 }).start(); 27 } 28 } 29 }
上述代碼僅僅在第11行定義方法處添加了synchronized關鍵字修飾該方法,程序運行結果如下:
結果表明多線程情況下,該代碼實現了真正的單例變量的創建。
注:實際情況下,只有第一次執行該方法時,才需要同步,後續的其他線程調用該方法時,此時的同步是沒有必要的。另外synchronized作爲重量級鎖,在同步方法時會帶來一定的性能影響。
解決方法二:使用“雙重檢查加鎖”方法
使用雙重檢查,首先判斷實例是否已經被創建,若沒有被創建則開始對對象加鎖實現同步。這樣的話,只會在首次創建實例時進行同步。
1 public class Singleton { 2 3 //私有化靜態實例變量 4 private static volatile Singleton instance; 5 6 //私有化構造方法,保證其他類無法創建該類的新對象 7 private Singleton() { 8 } 9 10 public static Singleton getInstance() { 11 //判斷實例是否存在,若不存在則創建一個新對象並返回 12 if (instance == null) { 13 //對Singleton.class對象加鎖 14 synchronized (Singleton.class) { 15 if (instance == null) { 16 //創建對象時,打印一下 17 System.out.println("創建了一個實例"); 18 instance = new Singleton(); 19 } 20 } 21 } 22 return instance; 23 } 24 25 public static void main(String[] args) { 26 //開啓10個線程,調用獲取實例方法 27 for (int i = 0; i < 10; i++) { 28 new Thread(() -> { 29 Singleton.getInstance(); 30 }).start(); 31 } 32 } 33 }
請注意上述代碼第4行,變量聲明時使用了volatile修飾,這是爲了保證多線程之間,某個對該變量的修改永遠會對其他線程可見,這裏涉及到happens-before原則還有重排序的知識,就不展開討論了,大家有興趣的可以自行百度看一下。
注:在1.4以及更早版本的java中,很多JVM對於volatile的實現會導致雙重檢查加鎖的失效,所以如果想使用雙重檢查加鎖,請保證java版本至少在1.5及以上!
解決方法三:基於類初始化的解決方案
JVM在類的初始化階段(即在Class被加載後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。基於這個特性,可以實現另一種線程安全的延遲初始化方案,這個方案被稱之爲Initialization On Demand Holder idiom)。代碼示例如下:
1 public class Singleton { 2 3 //私有化構造方法,保證其他類無法創建該類的新對象 4 private Singleton() { 5 } 6 7 //私有靜態內部類 8 private static class SingletonHolder { 9 //直接新建一個實例對象 10 private static final Singleton INSTANCE = new Singleton(); 11 } 12 13 public static Singleton getInstance() { 14 return SingletonHolder.INSTANCE; 15 } 16 17 public static void main(String[] args) { 18 //開啓10個線程,調用獲取實例方法 19 for (int i = 0; i < 10; i++) { 20 new Thread(() -> { 21 Singleton.getInstance(); 22 }).start(); 23 } 24 } 25 }
可以簡單理解爲,類對象在初始化時也會存在一個鎖,該鎖可以保證對類對象靜態變量的寫入對後續其他線程可見。
個人推薦使用解決方法二或者解決方法三。
參考資料:
《java併發編程的藝術》
《HeadFirst設計模式》