GOF23设计模式之单例模式(singleton)

一、单例模式概述

  保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

  由于单例模式只生成一个实例,减少了系统性能开销。所以当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

  单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,复制所有数据表的映射处理。

  1. 懒汉式

    1)线程不安全

    ① 构造器私有化,避免外部直接创建对象

    ② 声明一个私有的静态属性

    ③ 对外创建一个公共的静态方法访问该属性

      如果属性没有该对象,创建该对象

    2)线程安全 

      使用同步方法

    3)双重检测(线程安全)double checking DCL

      使用同步块

      好处:第一次是为了不必要的同步,第二次是在属性等于null的情况下才创建实例
  2. 饿汉式

    ① 构造器私有化,避免外部直接创建对象

    ② 声明一个私有的静态属性,同时创建该对象

    ③ 对外创建一个公共的静态方法访问该属性

      好处:类加载时就已经加载该类对象,当需要该对象时,若没有则创建,若有直接返回

  3. 静态内部类

    ① 构造器私有化,避免外部直接创建对象

    ② 使用静态内部类声明一个私有的静态属性,同时创建该对象

    ③ 对外创建一个公共的静态方法访问该属性

      好处:不调用静态方法不加载内部类,延缓加载,提高效率

  4.枚举

    枚举式 (jdk1.4及以前建议使用)

二、单例模式的五种写法

  1.饿汉式

    优点:线程安全,效率高

    缺点:无法延时加载

 1 public class Singleton {
 2     
 3     private static Singleton instance = new Singleton();
 4     
 5     //私有化构造器
 6     private Singleton() {}
 7 
 8     //提供全局访问点
 9     public static Singleton getInstance() {
10         return instance;
11     }
12     
13 }
 1 public class Singleton {
 2     
 3     private static Singleton instance = null;
 4     
 5     static {
 6         instance = new Singleton();
 7     }
 8     
 9     //私有化构造器
10     private Singleton() {}
11 
12     //提供全局访问点
13     public static Singleton getInstance() {
14         return instance;
15     }
16     
17 }

  2. 懒汉式

    优点:线程安全,延时加载

    缺点:效率较低

    (1)非线程安全

 1 public class Singleton {
 2     
 3     private static Singleton instance;
 4     
 5     //私有化构造器
 6     private Singleton() {}
 7 
 8     //提供一个全局的访问点
 9     public static Singleton getInstance() {
10         if (instance == null) {
11             instance = new Singleton();
12         }
13         return instance;
14     }
15     
16 }

    (2)线程安全

 1 public class Singleton {
 2     
 3     private static Singleton instance;
 4     
 5     //私有化构造器
 6     private Singleton() {}
 7 
 8     //使用同步方法获取该类对象
 9     public static synchronized Singleton getInstance() {
10         if (instance == null) {
11             instance = new Singleton();
12         }
13         return instance;
14     }
15     
16 }
 1 public class Singleton {
 2     
 3     private static Singleton instance;
 4     
 5     //私有化构造器
 6     private Singleton() {}
 7 
 8     //使用同步块获取该类对象
 9     public static Singleton getInstance() {
10         synchronized (Singleton.class) {
11             if (instance == null) {
12                 instance = new Singleton();
13             }
14             return instance;
15         }
16     }
17     
18 }

  3.双重检查锁

    注意:由于编译器优化和JVM底层内部模型原因,偶尔会出问题,不建议使用

    优点:线程安全,延时加载

    缺点:效率较低,会出错误

 1 public class Singleton {
 2     
 3     private static Singleton instance;
 4     
 5     private Singleton() {}
 6     
 7     //第一次判断是为了避免不必要的同步,第二次判断是属性为null时创建实例
 8     public static Singleton getInstance() {
 9         if (instance == null) {
10             synchronized (Singleton.class) {
11                 if (instance == null) {
12                     instance = new Singleton();
13                 }
14             }
15         }
16         return instance;
17     }
18 
19 }

  4.静态内部类式

    不调用静态方法不加载内部类,延缓加载(懒加载),提高效率

    优点:线程安全,延时加载,效率高

 1 public class Singleton {
 2     
 3     //静态内部类
 4     private static class SingletonHolder {
 5         //final可加可不加,因为外部类的外部无法使用该内部类
 6         private static /*final*/ Singleton instance = new Singleton();
 7     }
 8     
 9     //私有化构造器
10     private Singleton() {}
11 
12     //提供一个全局的访问点
13     public static Singleton getInstance() {
14         return SingletonHolder.instance;
15     }
16     
17 }

  5.枚举

    注意:建议使用

    优点:实现简单,线程安全,效率高,由于JVM从根本上实现保障,避免反射和反序列化的漏洞

    缺点:无延时加载

1 1 public enum Singleton {
2 2     //这个枚举元素,本身就是一个单例对象
3 3     INSTANCE;
4 4 }

三、测试五种单例模式的耗时问题

 1 /**
 2     在多线程环境下测试使用单例设计模式时创建对象的耗时(100个线程创建10000个对象)
 3        饿汉式:15ms
 4        懒汉式:671ms
 5        双重检测锁:65ms
 6        静态内部类:23ms
 7        枚举:32ms
 8  * @author CL
 9  *
10  */
11 public class TestRuntime {
12     
13     public static void main(String[] args) throws Exception {
14         long start = System.currentTimeMillis();
15         
16         int threadNum = 100;
17         final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
18         //匿名内部类
19         for (int i = 0; i < threadNum; i++) {
20             new Thread(new Runnable() {
21                 
22                 @Override
23                 public void run() {
24                     for(int i = 0; i < 10000; i++) {
25                             //依次测试
26                             Object o1 = SingletonDemo01.getInstance();    //饿汉式
27 //                            Object o2 = SingletonDemo02.getInstance();    //懒汉式
28 //                            Object o3 = SingletonDemo03.getInstance();    //双重检查锁
29 //                            Object o4 = SingletonDemo04.getInstance();    //静态内部类
30 //                            Object o5 = SingletonDemo05.INSTANCE;        //枚举
31                     }
32                 }
33             }).start();
34             countDownLatch.countDown();    //递减锁存器的计数,如果计数到达零,则释放所有等待的线程
35         }
36         
37         countDownLatch.await();    //阻塞main线程,知道计数器为0才开始继续执行
38         
39         long end = System.currentTimeMillis();
40         System.out.println("总耗时:"+(end - start)+"ms");
41     }
42 
43 }

  注意,根据电脑配置等因素时间会有差异,但时间比大致相同。

  如何选用单例模式?

    (1)单例对象占用资源少,不需要延时加载时:枚举式好于饿汉式

    (2)单例对象占用资源大,需要延时加载时:静态内部类式好于懒汉式

四、破解单例模式 

  1.使用反序列化破解单例模式

    步骤:(1)使用序列化将已经创建的对象写出到系统文件

          (2)使用反序列化读取文件中的对象

 1 import java.io.Serializable;
 2 
 3 /**
 4  * 使用反序列化破解单例模式
 5  * @author CL
 6  *
 7  */
 8 public class Singleton implements Serializable {
 9     
10     private static class SingletonHolder {
11         private static Singleton instance = new Singleton();
12     }
13     
14     private Singleton() {}
15 
16     public static Singleton getInstance() {
17         return SingletonHolder.instance;
18     }
19     
20 }
 1 import java.io.FileInputStream;
 2 import java.io.FileOutputStream;
 3 import java.io.ObjectInputStream;
 4 import java.io.ObjectOutputStream;
 5 
 6 /**
 7  * 测试反序列化破解单例模式
 8  * @author CL
 9  *
10  */
11 public class TestSingleton {
12     
13     public static void main(String[] args) throws Exception {
14         //先获得一个对象
15         Singleton s01 = Singleton.getInstance();
16         System.out.println("先获取的对象:" + s01);
17         
18         //(1)使用序列化将对象写出到系统文件
19         String filePath = "C:\file\obj.txt";
20         ObjectOutputStream oos = new ObjectOutputStream(
21                 new FileOutputStream(filePath));
22         oos.writeObject(s01);
23         oos.flush();
24         oos.close();
25         
26         //(2)使用反序列化读取文件中的对象
27         ObjectInputStream ois = new ObjectInputStream(
28                 new FileInputStream(filePath));
29         Singleton s02 = (Singleton) ois.readObject();
30         ois.close();
31         
32         System.out.println("反序列化读取的对象:" + s02);
33     }
34 
35 }

  控制台输出:

先获取的对象:com.caolei.singleton.Singleton@15db9742
反序列化读取的对象:com.caolei.singleton.Singleton@33909752

  显然已经通过反序列化已经创建了新的对象,解决办法是在类中添加如下代码:

1 /**
2  * 反序列化时,如果创建了readResolve()方法则直接返回已经创建好的对象,而不需要再重新创建新的对象
3  * @return
4  * @throws ObjectStreamException
5  */
6  private Object readResolve() throws ObjectStreamException {
7      return instance;
8  }

  现在再测试:

 1 import java.io.ObjectStreamException;
 2 import java.io.Serializable;
 3 
 4 /**
 5  * 使用反序列化破解单例模式
 6  * @author CL
 7  *
 8  */
 9 public class Singleton implements Serializable {
10     
11     private static class SingletonHolder {
12         private static Singleton instance = new Singleton();
13     }
14     
15     private Singleton() {}
16 
17     public static Singleton getInstance() {
18         return SingletonHolder.instance;
19     }
20     
21     //反序列化时,如果创建了readResolve()方法则直接返回已经创建好的对象,而不需要再重新创建新的对象
22     private Object readResolve() throws ObjectStreamException {
23         return SingletonHolder.instance;
24     }
25     
26 }
 1 import java.io.FileInputStream;
 2 import java.io.FileOutputStream;
 3 import java.io.ObjectInputStream;
 4 import java.io.ObjectOutputStream;
 5 
 6 /**
 7  * 测试反序列化破解单例模式
 8  * @author CL
 9  *
10  */
11 public class TestSingleton {
12     
13     public static void main(String[] args) throws Exception {
14         //先获得一个对象
15         Singleton s01 = Singleton.getInstance();
16         System.out.println("先获取的对象:" + s01);
17         
18         //(1)使用序列化将对象写出到系统文件
19         String filePath = "C:\file\obj.txt";
20         ObjectOutputStream oos = new ObjectOutputStream(
21                 new FileOutputStream(filePath));
22         oos.writeObject(s01);
23         oos.flush();
24         oos.close();
25         
26         //(2)使用反序列化读取文件中的对象
27         ObjectInputStream ois = new ObjectInputStream(
28                 new FileInputStream(filePath));
29         Singleton s02 = (Singleton) ois.readObject();
30         ois.close();
31         
32         System.out.println("反序列化读取的对象:" + s02);
33     }
34 
35 }

  控制台输出:

先获取的对象:com.caolei.singleton.Singleton@15db9742
反序列化读取的对象:com.caolei.singleton.Singleton@15db9742

  现在创建的对象是同一对象,避免了反序列化破解单例模式出现的问题。

  2.使用反射破解单例模式

    步骤:(1)获取对象

          (2)获取构造器

          (3)跳过安全检查

          (4)创建对象

 1 /**
 2  * 使用反射破解单例模式
 3  * @author CL
 4  *
 5  */
 6 public class Singleton {
 7     
 8     private static class SingletonHolder {
 9         private static Singleton instance = new Singleton();
10     }
11     
12     private Singleton() {}
13 
14     public static Singleton getInstance() {
15         return SingletonHolder.instance;
16     }
17     
18 }
 1 import java.lang.reflect.Constructor;
 2 
 3 /**
 4  * 测试反射破解单例模式
 5  * @author CL
 6  *
 7  */
 8 public class TestSingleton {
 9 
10     public static void main(String[] args) throws Exception {
11         Singleton s1 = Singleton.getInstance();
12         System.out.println("先获取的对象:" + s1);
13 
14         //使用反射破解单例模式
15         //(1)创建对象
16         Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.caolei.singleton.Singleton");
17         //(2)获得构造器
18         Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
19         //(3)跳过安全检查
20         c.setAccessible(true);
21         //(4)创建对象
22         Singleton s2 = c.newInstance();
23         
24         System.out.println("反射获取的对象:" + s2);
25     }
26 }

  控制台输出:

先获取的对象:com.caolei.singleton.Singleton@15db9742
反射获取的对象:com.caolei.singleton.Singleton@6d06d69c

  显然使用反射机制跳过安全检查通过私有的构造器创建了新的对象,解决办法是在私有的构造器中添加如下代码:

1 if (instance != null) {
2     throw new RuntimeException();    //再次创建对象时抛出异常
3 }

  现在再测试:

 1 /**
 2  * 使用反射破解单例模式
 3  * @author CL
 4  *
 5  */
 6 public class Singleton {
 7     
 8     private static class SingletonHolder {
 9         private static Singleton instance = new Singleton();
10     }
11     
12     private Singleton() {
13         if (SingletonHolder.instance != null) {
14             throw new RuntimeException();    //再次创建对象时抛出异常
15         }
16     }
17 
18     public static Singleton getInstance() {
19         return SingletonHolder.instance;
20     }
21     
22 }
 1 import java.lang.reflect.Constructor;
 2 
 3 /**
 4  * 测试反射破解单例模式
 5  * @author CL
 6  *
 7  */
 8 public class TestSingleton {
 9 
10     public static void main(String[] args) throws Exception {
11         Singleton s1 = Singleton.getInstance();
12         System.out.println("先获取的对象:" + s1);
13 
14         //使用反射破解单例模式
15         //(1)创建对象
16         Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.caolei.singleton.Singleton");
17         //(2)获得构造器
18         Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
19         //(3)跳过安全检查
20         c.setAccessible(true);
21         //(4)创建对象
22         Singleton s2 = c.newInstance();
23         
24         System.out.println("反射获取的对象:" + s2);
25     }
26 }

  控制台输出:

先获取的对象:com.caolei.singleton.Singleton@15db9742
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
    at com.caolei.singleton.TestSingleton.main(TestSingleton.java:24)
Caused by: java.lang.RuntimeException
    at com.caolei.singleton.Singleton.<init>(Singleton.java:16)
    ... 5 more

 五、单例模式常见的应用场景

  (1)Windows的Task Manager(任务管理器)、Recycle Bin(回收站)、文件系统等都是典型的单例模式;

  (2)在项目中,读取配置文件的类一般也只有一个对象,没有必要每次使用配件的数据时,都去new一个对象来获取;

  (3)网站的计数器,一般也是采用单例模式实现,否则难以同步;

  (4)数据库连接池的设计就是采用单例模式;

  (5)在Spring中,每个Bean默认是单例的,便于Spring容器管理;

  (6)每个Servlet都是单例的;

  (7)………………

原文地址:https://www.cnblogs.com/cao-lei/p/8085529.html