设计模式(二)——单例模式(史上最全)

一、概述

1、介绍

  所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。

2、优缺点

  优点:提供了对唯一实例的受控访问;由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时,单例模式无疑可以提高系统的性能;避免对共享资源的多重占用。
  缺点:不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态;由于单利模式中没有抽象层,因此单例类的扩展有很大的困难;单例类的职责过重,在一定程度上违背了"单一职责原则"。

3、应用场景

  网站的计数器;Web应用的配置对象的读取,由于配置文件是共享的资源;数据库连接池的设计,因为数据库连接是一种数据库资源;多线程的线程池的设计,这是由于线程池要方便对池中的线程进行控制。

二、创建方式

1、饿汉式(静态常量)(线程安全)

 1 public class Singleton {
 2 
 3     private static final Singleton instance = new Singleton();
 4 
 5     private Singleton() {
 6     }
 7 
 8     public static Singleton getInstance() {
 9         return instance;
10     }
11 }

  优点:类初始化时,会立即加载该对象,仅实例化一次。效率高,获取实例的速度快,线程是安全的。
  缺点:类加载的时候立即实例化对象,可能实例化的对象不会被使用,可能造成内存的浪费。
  结论:可用,不推荐。

2、饿汉式(静态代码块)(线程安全)

 1 public class Singleton {
 2 
 3     private static final Singleton instance;
 4 
 5     private Singleton() {
 6     }
 7     
 8     static {
 9         instance = new Singleton();
10     }
11 
12     public static Singleton getInstance() {
13         return instance;
14     }
15 
16 }

  优点:在类装载的时候,就执行静态代码块中的代码,初始化类的实例。线程是安全的。
  缺点:类加载的时候立即实例化对象,可能实例化的对象不会被使用,可能造成内存的浪费。
  结论:可用,不推荐。

3、懒汉式(线程不安全)

 1 public class Singleton {
 2 
 3     private static Singleton instance;
 4 
 5     private Singleton() {
 6     }
 7 
 8     public static Singleton getInstance() {
 9         if (instance == null) {
10             instance = new Singleton();
11         }
12 
13         return instance;
14     }
15 }

  优点:使用的时候,创建对象,节省系统资源。
  缺点:线程不安全。
  结论:不可用。

4、懒汉式(线程安全,同步方法)

 1 public class Singleton {
 2     private static Singleton instance;
 3 
 4     private Singleton() {
 5     }
 6 
 7     public static synchronized Singleton getInstance() {
 8         if (instance == null) {
 9             instance = new Singleton();
10         }
11         return instance;
12     }
13 }

  优点:线程安全。
  缺点:有同步锁,效率低。
  结论:可用,不推荐。

5、懒汉式(线程安全,同步代码块)

 1 public class Singleton {
 2     private static Singleton instance;
 3 
 4     private Singleton() {
 5     }
 6 
 7     public static Singleton getInstance() {
 8         synchronized (Singleton.class) {
 9             if (instance == null) {
10                 instance = new Singleton();
11             }
12         }
13 
14         return instance;
15     }
16 }

  优点:线程安全。
  缺点:有同步锁,效率低。
  结论:可用,不推荐。

6、双重锁(线程安全)

 1 public class Singleton {
 2     private static volatile Singleton instance;
 3 
 4     private Singleton() {
 5     }
 6 
 7     public static Singleton getInstance() {
 8         if (instance == null) {
 9             synchronized (Singleton.class) {
10                 if (instance == null) {
11                     instance = new Singleton();
12                 }
13             }
14         }
15         return instance;
16     }
17 }

  优点:线程安全。当实例存在的时候,可用不走同步锁,减少使用锁带来的性能的消耗。延迟加载,效率较高。
  缺点:无。
  结论:可用,推荐。

  对volatile的说明(重要):
  不加volatile有可能会进行指令重排序。指令重排:一般而言初始化操作并不是一个原子操作,而是分为三步:
  step1:在堆中开辟对象所需空间,分配地址。
  step2:根据类加载的初始化顺序进行初始化。
  step3:将内存地址返回给栈中的引用变量。

  由于 Java 内存模型允许"无序写入",有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了:
  step1:
  step3:(此时的 instance 就不是null,但变量并没有初始化完成)。
  step2:
  所以就可能会出现以下情况(有问题):

时间轴 Thread1 Thread2
1 第一次检测, instance 为空  
2 获取锁  
3 再次检测, instance 为空  
4 在堆中分配内存空间  
5 instance 指向分配的内存空间  
6   第一次检测,instance不为空
7   访问 instance(此时对象还未初始化完成)

  总结:不加volatile,会有问题!加入volatile关键字修饰之后,会禁用指令重排,这样就保证单例的正确性。关于更多volatile,请学习JMM。

7、静态内部类(线程安全)

 1 public class Singleton {
 2 
 3     private Singleton() {
 4     }
 5 
 6     private static class SingletonInstance {
 7         private static final Singleton INSTANCE = new Singleton();
 8     }
 9 
10     public static Singleton getInstance() {
11         return SingletonInstance.INSTANCE;
12     }
13 }

  这种方式采用了类装载的机制来保证初始化实例时只有一个线程。静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时。调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
  类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

  优点:线程安全(不会被反射入侵)。延迟加载,效率高。
  缺点:需要两个类完成,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久代的对象。
  结论:可用,推荐。

8、枚举(线程安全)

1 public enum Singleton {
2     INSTANCE;
3 
4     public void sayOK() {
5         System.out.println("ok~");
6     }
7 }

  枚举本身就是单例的,一般在项目中定义常量。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。这种方式是Effective Java作者Josh Bloch 提倡的方式 。
  结论:可用,推荐。

三、单例的破坏

  单例模式一定能保证只有一个实例对象吗?答案:不能。
  破坏单例的两种方式:反射、反序列化。

1、反射破坏

  通过反射是可以破坏单例的,例如使用内部类实现的单例。通过反射获取其默认的构造函数,然后使默认构造函数可访问,就可以创建新的对象了。
  代码示例:反射破坏单例

 1 public class Main {
 2     public static void main(String[] args) throws Exception {
 3         Singleton instance = Singleton.getInstance();
 4 
 5         final Class<Singleton> aClass = Singleton.class;
 6 
 7         // 获取默认的构造方法
 8         Constructor<Singleton> constructor = aClass.getDeclaredConstructor();
 9         // 使默认构造方法可访问
10         constructor.setAccessible(true);
11 
12         // 创建对象
13         final Singleton instance1 = constructor.newInstance();
14 
15         System.out.println(instance == instance1);
16     }
17 }
18 
19 // 结果
20 false

  代码示例:阻止反射破坏单例

 1 public class ReflectionSingleton {
 2 
 3     // 标志位
 4     private static boolean flag = false;
 5 
 6     private static ReflectionSingleton instance;
 7 
 8     private ReflectionSingleton() {
 9         synchronized (ReflectionSingleton.class) {
10             if (!flag) {
11                 flag = true;
12             } else {
13                 throw new RuntimeException("单例模式被破坏!");
14             }
15         }
16     }
17 
18     public static ReflectionSingleton getInstance() {
19         if (instance == null) {
20             instance = new ReflectionSingleton();
21         }
22 
23         return instance;
24     }
25 }

  注意:上面可以阻止单例的破坏,但是有一个BUG,如果先用的反射,再用getInstance()获取单例,就会报错。这种写法除非能保证getInstance先于反射执行。

  代码示例:反射先于获取单例的形式

 1 public class Main2 {
 2     public static void main(String[] args) throws Exception {
 3 
 4         final Class<ReflectionSingleton> aClass = ReflectionSingleton.class;
 5 
 6         // 获取默认的构造方法
 7         Constructor<ReflectionSingleton> constructor = aClass.getDeclaredConstructor();
 8         // 使默认构造方法可访问
 9         constructor.setAccessible(true);
10 
11         // 创建对象
12         ReflectionSingleton instance2 = constructor.newInstance();
13         System.out.println("反射实例:" + instance2);
14 
15         // 再次调用
16         ReflectionSingleton instance = ReflectionSingleton.getInstance();
17         System.out.println(instance == instance2);
18     }
19 }
20 
21 // 结果
22 Exception in thread "main" java.lang.RuntimeException: 单例模式被破坏!

2、反序列化破坏

  Singleton 要实现Serializable接口。
  代码示例:反序列化破坏单例

 1 public class Main {
 2     public static void main(String[] args) throws Exception {
 3         // 序列化
 4         Singleton instance1 = Singleton.getInstance();
 5 
 6         ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile.txt"));
 7         out.writeObject(Singleton.getInstance());
 8 
 9         ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("tempfile.txt")));
10 
11         // 调用readObject()反序列化
12         Singleton instance2 = (Singleton) in.readObject();
13 
14         System.out.println(instance1 == instance2); // 结果是:false
15     }
16 }
17 
18 // 结果
19 false

  代码示例:阻止反序列化破坏单例
  只需要在Singleton类中,实现自己的readResolve()方法即可。

1 public Object readResolve() {
2     return instance;
3 }

 

上一篇:枚举实现创建单例


下一篇:单例模式