單例模式(Singleton)

1、單例模式介紹html

單例模式:保證一個類只有一個實例,而且提供一個訪問該實例的全局訪問點。java

單例模式優勢:數據庫

1.只生成一個實例,系統開銷比較小windows

2.單例模式能夠在系統設置全局的訪問點,優化共享資源的訪問。設計模式

常見單例模式分類:安全

主要:併發

餓漢式(線程安全,調用效率高,可是不能延時加載)ide

懶漢式(線程安全,調用效率不高,可是能夠延時加載高併發

其餘:學習

雙重檢測鎖式(因爲JVM底層內部模型緣由,偶爾會出問題。不建議使用)

靜態內部類式(線程安全,調用效率高。可是能夠延時加載)

枚舉單例(線程安全,調用效率高,不能延時加載)

 

2、單例模式實例代碼

一、懶漢式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.fz.singleton;
 
/**
  * 餓漢式單例:所謂餓漢式,就是比較餓。當類一加載的時候就直接new了一個靜態實例。無論後面有沒有用到該實例
  */
public class Singleton1 {
     /**
      * 一、提供一個靜態變量。
      * 當類加載器加載該類時,就new一個實例出來。從屬於這個類。無論後面用不用這個類。因此沒有延時加載功能
      */
     private static Singleton1 instance = new Singleton1();
     /**
      * 二、私有化構造器:外部是不能直接new該對象的
      */
     private Singleton1(){}
     /**
      * 三、對外提供一個公共方法來獲取這個惟一對象(方法沒有使用synchronized則調用效率高)
      * @return
      */
     public static Singleton1 getInstance(){
         return instance;
     }
}

二、餓漢式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.fz.singleton;
 
/**
  * 懶漢式單例:比較懶,一開始不初始化實例。等何時用就何時初始化.避免資源浪費
  */
public class Singleton2 {
     /**
      * 一、聲明一個靜態實例,不給它初始化。等何時用就何時初始化。節省資源
      */
     private static Singleton2 instance;
     /**
      * 二、依然私有化構造器,對外不讓new
      */
     private Singleton2(){}
     /**
      * 三、對外提供一個獲取實例的方法,由於靜態屬性沒有實例化。
      * 假如高併發的時候,有可能會同時調用該方法。形成new出多個實例。因此須要加上同步synchronized,所以調用效率不高
      * 在方法上加同步,是整個方法都同步。效率不高
      * @return
      */
     public synchronized static Singleton2 getInstance(){
         if (instance == null ) { //第一次調用時爲空,則直接new一個
             instance = new Singleton2();
         }
         //以後第二次再調用的時候就已經初始化了,不用再new。直接返回
         return instance;
     }
}

三、雙重檢索方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.fz.singleton;
/**
  * 雙重檢索單例模式
  * 將鎖加在判斷實例爲空的地方,不加在方法上
  */
public class Singleton3 {
     /**
      * 一、提供未實例化的靜態實例
      */
     private static Singleton3 instance = null ;
     /**
      * 二、私有化構造器
      */
     private Singleton3(){}
     /**
      * 三、對外提供獲取實例的方法
      * 可是同步的時候將鎖放到第一次獲取實例的時候,這樣的好處就是隻有第一次會同步。效率高
      * @return
      */
     public static Singleton3 getInstance(){
         if (instance == null ) {
             Singleton3 s3;
             synchronized (Singleton3. class ) {
                 s3 = instance;
                 if (s3 == null ) {
                     synchronized (Singleton3. class ) {
                         if (s3 == null ) {
                             s3 = new Singleton3();
                         }
                     }
                     instance = s3;
                 }
             }
         }
         return instance;
     }
 
}

四、靜態內部類方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.fz.singleton;
/**
  * 靜態內部類單例實現
  */
public class Singleton4 {
     
     /**
      * 一、私有化構造器
      */
     private Singleton4(){}
     /**
      * 二、聲明一個靜態內部類,在靜態內部類內部提供一個外部類的實例(常量,不可改變)
      * 初始化Singleton4 的時候不會初始化SingletonClassInstance,實現了延時加載。而且線程安全
      */
     private static class SingletonClassInstance{
         //該實例只讀,無論誰都不能修改
         private static final Singleton4 instance = new Singleton4();
     }
     /**
      * 三、對外提供一個獲取實例的方法:直接返回靜態內部類中的那個常量實例
      * 調用的時候沒有同步等待,因此效率也高
      * @return
      */
     public static Singleton4 getInstance(){
         return SingletonClassInstance.instance;
     }
 
}

五、枚舉單例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.fz.singleton;
/**
  * 枚舉實現單例模式(枚舉自己就是單例)
  */
public enum Singleton5 {
     /**
      * 定義一個枚舉元素,它就是一個單例的實例了。
      */
     INSTANCE;
     
     /**
      * 對枚舉的一些操做
      */
     public void singletonOperation(){
         
     }
     
}

 

3、如何破解單例模式?

a、經過反射破解(不包括枚舉,由於枚舉自己是單例,是由JVM管理的)

b、經過反序列化

一、經過反射破解單例實例代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.fz.singleton;
 
import java.lang.reflect.Constructor;
 
/**
  * 經過反射破解單例模式
  */
public class TestReflect {
     public static void main(String[] args) throws Exception {
         Singleton6 s1 = Singleton6.getInstance();
         Singleton6 s2 = Singleton6.getInstance();
         System.out.println(s1 == s2); //true
         
         //經過反射破解
         Class<Singleton6> clazz = (Class<Singleton6>) Class.forName(Singleton6. class .getName());
         Constructor<Singleton6> c = clazz.getDeclaredConstructor( null ); //得到無參構造器
         c.setAccessible( true ); //跳過檢查:能夠訪問private構造器
         Singleton6 s3 = c.newInstance(); //此時會報錯:沒有權限訪問私有構造器
         Singleton6 s4 = c.newInstance();
         System.out.println(s3==s4); //不加c.setAccessible(true)則會報錯。此時的結果就是false,得到的就是兩個對象
         
     }
}

如何防止反射破解單例模式呢?

在Singleton6構造的時候,假如不是第一次就直接拋出異常。不讓建立。這樣第二次構建的話就直接拋出異常了。

1
2
3
4
5
6
private Singleton6(){
     if (instance != null ) {
         //若是不是第一次構建,則直接拋出異常。不讓建立
         throw new RuntimeException();
     }
}

二、經過序列化和反序列化構建對象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.fz.singleton;
 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
 
/**
  * 經過反射破解單例模式
  */
public class TestReflect {
     public static void main(String[] args) throws Exception {
         Singleton6 s1 = Singleton6.getInstance();
         Singleton6 s2 = Singleton6.getInstance();
 
         //經過反序列化構建對象:經過序列化將s1存儲到硬盤上,而後再經過反序列化把s1再構建出來
         FileOutputStream fos = new FileOutputStream( "e:/a.txt" );
         ObjectOutputStream oos = new ObjectOutputStream(fos);
         oos.writeObject(s1);
         oos.close();
         fos.close();
         //經過反序列化將s1對象再構建出來
         ObjectInputStream ois = new ObjectInputStream( new FileInputStream( "e:/a.txt" ));
         Singleton6 s5 = (Singleton6) ois.readObject();
         System.out.println(s5); //此時打印出一個新對象
         System.out.println(s1==s5); //false
     }
}

防止反序列化構建對象

在Singleton6中定義一個方法,此時結果就會同樣了。System.out.println(s1==s5);結果就是true了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.fz.singleton;
 
import java.io.ObjectStreamException;
import java.io.Serializable;
 
/**
  * 用於測試反射破解的單例類
  */
public class Singleton6 implements Serializable {
     /**
      * 一、提供一個靜態變量。
      * 當類加載器加載該類時,就new一個實例出來。從屬於這個類。無論後面用不用這個類。因此沒有延時加載功能
      */
     private static Singleton6 instance = new Singleton6();
     /**
      * 二、私有化構造器:外部是不能直接new該對象的
      */
     private Singleton6(){
         if (instance != null ) {
             //若是不是第一次構建,則直接拋出異常。不讓建立
             throw new RuntimeException();
         }
     }
     /**
      * 三、對外提供一個公共方法來獲取這個惟一對象(方法沒有使用synchronized則調用效率高)
      * @return
      */
     public static Singleton6 getInstance(){
         return instance;
     }
     
     /**
      * 反序列化時,若是定義了readResolve()則直接返回該方法指定的實例。不會再單首創建新對象!
      * @return
      * @throws ObjectStreamException
      */
     private Object readResolve() throws ObjectStreamException{
         return instance;
     }
     
}

測試幾種單例的速度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.fz.singleton;
  
import java.util.concurrent.CountDownLatch;
  
/**
  * 測試幾種單例模式的速度
  */
public class TestSingleton {
     public static void main(String[] args) throws InterruptedException {
         long start = System.currentTimeMillis();
         int threadNum = 10 ; //10個線程
         final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
          
         for ( int i = 0 ; i < threadNum; i++) {
             new Thread( new Runnable() {
                 @Override
                 public void run() {
                     for ( int i = 0 ; i < 100000 ; i++) {
                         Object o = Singleton5.INSTANCE;
                     }
                     countDownLatch.countDown(); //計數器-1
                 }
             }).start();
         }
          
         countDownLatch.await(); //main線程阻塞
         long end = System.currentTimeMillis();
         System.out.println( "耗時:" +(end-start));
          
         /**
          * 結果(毫秒):
          * Singleton1(餓漢式)耗時:5
          * Singleton2(懶漢式)耗時:227
          * Singleton3(雙重檢索式)耗時:7
          * Singleton4(靜態內部類式)耗時:40
          * Singleton5(枚舉式)耗時:5
          */
     }
}

 

4、總結

如何選用?

        枚舉式  好於  餓漢式

        靜態內部類式  好於 懶漢式

常見應用場景

        ​windows的任務管理器

        網站的計數器

        數據庫的鏈接池

        Application容器也是單例

        Spring中每一個bean默認也是單例

        Servlet中,每一個servlet也是單例

 


Java23種設計模式學習筆記【目錄總貼】

參考資料:

  大話設計模式(帶目錄完整版).pdf

  HEAD_FIRST設計模式(中文版).pdf

  尚學堂_高淇_java300集最全視頻教程_【GOF23設計模式】

相關文章
相關標籤/搜索