單例模式(Singleton)


一、單例模式介紹

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

單例模式優點:

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

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

常見單例模式分類:

主要:

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

懶漢式(線程安全,調用效率不高,但是可以延時加載

其他:

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

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

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

 

二、單例模式實例代碼

1、懶漢式

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 {
     /**
      * 1、提供一個靜態變量。
      * 當類加載器加載該類時,就new一個實例出來。從屬於這個類。不管后面用不用這個類。所以沒有延時加載功能
      */
     private static Singleton1 instance = new Singleton1();
     /**
      * 2、私有化構造器:外部是不能直接new該對象的
      */
     private Singleton1(){}
     /**
      * 3、對外提供一個公共方法來獲取這個唯一對象(方法沒有使用synchronized則調用效率高)
      * @return
      */
     public static Singleton1 getInstance(){
         return instance;
     }
}

2、餓漢式

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 {
     /**
      * 1、聲明一個靜態實例,不給它初始化。等什么時候用就什么時候初始化。節省資源
      */
     private static Singleton2 instance;
     /**
      * 2、依然私有化構造器,對外不讓new
      */
     private Singleton2(){}
     /**
      * 3、對外提供一個獲取實例的方法,因為靜態屬性沒有實例化。
      * 假如高並發的時候,有可能會同時調用該方法。造成new出多個實例。所以需要加上同步synchronized,因此調用效率不高
      * 在方法上加同步,是整個方法都同步。效率不高
      * @return
      */
     public synchronized static Singleton2 getInstance(){
         if (instance == null ) { //第一次調用時為空,則直接new一個
             instance = new Singleton2();
         }
         //之后第二次再調用的時候就已經初始化了,不用再new。直接返回
         return instance;
     }
}

3、雙重檢索方式

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 {
     /**
      * 1、提供未實例化的靜態實例
      */
     private static Singleton3 instance = null ;
     /**
      * 2、私有化構造器
      */
     private Singleton3(){}
     /**
      * 3、對外提供獲取實例的方法
      * 但是同步的時候將鎖放到第一次獲取實例的時候,這樣的好處就是只有第一次會同步。效率高
      * @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;
     }
 
}

4、靜態內部類方式

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 {
     
     /**
      * 1、私有化構造器
      */
     private Singleton4(){}
     /**
      * 2、聲明一個靜態內部類,在靜態內部類內部提供一個外部類的實例(常量,不可改變)
      * 初始化Singleton4 的時候不會初始化SingletonClassInstance,實現了延時加載。並且線程安全
      */
     private static class SingletonClassInstance{
         //該實例只讀,不管誰都不能修改
         private static final Singleton4 instance = new Singleton4();
     }
     /**
      * 3、對外提供一個獲取實例的方法:直接返回靜態內部類中的那個常量實例
      * 調用的時候沒有同步等待,所以效率也高
      * @return
      */
     public static Singleton4 getInstance(){
         return SingletonClassInstance.instance;
     }
 
}

5、枚舉單例

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(){
         
     }
     
}

 

三、如何破解單例模式?

a、通過反射破解(不包括枚舉,因為枚舉本身是單例,是由JVM管理的)

b、通過反序列化

1、通過反射破解單例實例代碼

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();
     }
}

2、通過序列化和反序列化構建對象

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 {
     /**
      * 1、提供一個靜態變量。
      * 當類加載器加載該類時,就new一個實例出來。從屬於這個類。不管后面用不用這個類。所以沒有延時加載功能
      */
     private static Singleton6 instance = new Singleton6();
     /**
      * 2、私有化構造器:外部是不能直接new該對象的
      */
     private Singleton6(){
         if (instance != null ) {
             //如果不是第一次構建,則直接拋出異常。不讓創建
             throw new RuntimeException();
         }
     }
     /**
      * 3、對外提供一個公共方法來獲取這個唯一對象(方法沒有使用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
          */
     }
}

 

四、總結

如何選用?

        枚舉式  好於  餓漢式

        靜態內部類式  好於 懶漢式

常見應用場景

        ​windows的任務管理器

        網站的計數器

        數據庫的連接池

        Application容器也是單例

        Spring中每個bean默認也是單例

        Servlet中,每個servlet也是單例

 


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

參考資料:

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

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

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




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM