• 设计模式(二)——单例模式


    一、概述

    1、介绍

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

    2、优缺点

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

    3、应用场景

      网站的计数器;
      Web应用用于读取配置文件的类,因为配置文件是共享的资源;
      数据库连接池的设计,因为数据库连接是一种数据库资源;
      Spring中,每个bean默认都是单例的,这样便于Spring进行管理。
      多线程的线程池的设计,这是由于线程池要方便对池中的线程进行控制。

    二、创建方式

    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 }

    作者:Craftsman-L

    本博客所有文章仅用于学习、研究和交流目的,版权归作者所有,欢迎非商业性质转载。

    如果本篇博客给您带来帮助,请作者喝杯咖啡吧!点击下面打赏,您的支持是我最大的动力!

  • 相关阅读:
    Linux性能调优
    Linux动态库搜索路径的技巧
    [转]Linux动态库的种种要点
    [转]谈谈Linux下动态库查找路径的问题
    性能测试的几种业务模型设计
    性能测试解惑之并发压力
    一个系统的最大并发用户数为1100,怎么能推算出该系统的支持最大用户数
    IP欺骗
    关于Cocos2d-x随机数的生成
    关于Cocos2d-x节点和精灵节点的坐标、位置以及大小的设置
  • 原文地址:https://www.cnblogs.com/originator/p/15314055.html
Copyright © 2020-2023  润新知