設計模式筆記(一):單例模式

  單例模式可以說是設計模式中最簡單的設計模式之一了。顧名思義,單例模式指的是一個類只提供一個固定的單個實例,大家共用該實例。

  單例模式代碼實現步驟:

  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設計模式》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章