欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

单例模式的各种写法总结

程序员文章站 2022-07-13 23:39:20
...

1. 单例模式的概念

单例模式,是设计模式中最简单的一种。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。

1.1 单例模式中的几个要素

  • 私有构造方法(不能被实例化,阻止对象的生成)
  • 指向自己实例的私有静态引用( 在其内部产生该类的实例化对象,并将其封装成private static类型)
  • 定义一个静态的公有的方法返回该类的实例。

1.2 单例模式的优势

  • 内存空间占用的优化,无论在哪里在何时都是同一个对象同一个实例,节省了内存
  • 避免频繁的创建销毁对象,可以提高性能
  • 全局的同步化,由于是用同一个对象,对象的某些状态得到了同步,也就是说避免了不一致状态(全局访问)

1.3 单例模式的使用场景

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。(日志对象,Web应用配置对象)

2. 单例模式的各种写法

  • 第一种: 饿汉式单例(所谓饿汉可以这样理解 就是当类被加载的时候就初始化对象,比较饥渴难耐)
   public class Singleton{
     private static Singleton instance = new Singleton();
     private Singleton(){}
     public static Singleton getSingleton(){
         return instance;
     }
 }

优点:简单,线程安全, 避免了synchronized所造成的性能问题。
缺点:占用内存, 不管该资源是否被请求,它都会创建一个对象,占用jvm内存,以空间换时间,没有用到懒加载的思想。

  • 第二种:饱汉式(饱是相对于饿来说的,单线程,线程不安全)
public class Singleton{
    private static instance =null;
    private Singleton(){}
    public static Singleton getSingleton(){
      if(instance==null){
         instance = new Singleton();
      }
      return instance;
    }
}

优点:简单,节约内存,实现了懒加载思想。
缺点:线程不安全,多线程情况下可能出现多个实例。

  • 第三种:饱汉式改良版1(方法加锁,线程安全)
public class Singleton{
    private static instance =null;
    private Singleton(){}
    public static synchronized Singleton getSingleton(){
      if(instance==null){
         instance = new Singleton();
      }
      return instance;
    }
}

优点:简单,粗暴,好理解。
缺点:只需要在可能发生线程不安全的代码块加锁,没有必要在方法上加锁,一旦instance不为null,也就不不需要同步,这样系统的开销太大,效率低下。

  • 第四种:饱汉式改良版2(代码块加锁,线程安全)
public class Singleton{
    private static  Singleton instance =null;
    private Singleton(){}
    public static Singleton getSingleton(){
      if(instance==null){  //1
        synchronized(Singleton.class){
          instance = new Singleton();
        }
      }
      return instance;
    }
}

优点:减少了锁的开销。
缺点:代码并不完美,存在bug,考虑如下情形:
a:线程1执行到1挂起,线程1认为singleton为null
b:线程2执行到1挂起,线程2认为singleton为null
c:线程1被唤醒执行synchronized块代码,走完创建了一个对象
d:线程2被唤醒执行synchronized块代码,走完创建了另一个对象

  • 第五种:饱汉式改良版3(双重检查锁机制)
public class Singleton{
    private static Singleton instance =null;
    private Singleton(){}
    public static Singleton getSingleton(){
      if(instance==null){
        synchronized(Singleton.class){
          if(instance==null){
             instance = new Singleton();
          }     
        }
      }
      return instance;
    }
}

优点:能修复方法四的Bug,兼容了性能和并发的要求。
缺点隐藏了一个很深的Bug
bug 分析:当你执行instance = new SingletonC();这个操作的时候,实际上执行了三个步骤,
1. 分配内存,
2. 初始化数据
3. 把instance 指向内存

由于JVM 执行的时候可以进行指令重排(java 内存模型)假设我们考虑这样的情形:当线程1 执行完步骤1,3,然后此时线程2进来判断instance不为空直接返回instance,很显然,此时的instance 还没有初始化,这是有问题的。

  • 第六种:饱汉式改良版4(双重检查锁机制 +volatile,(强烈推荐))
public class Singleton{
    private static volatile Singleton instance =null;
    private Singleton(){}
    public static Singleton getSingleton(){
      if(instance==null){
        synchronized(Singleton.class){
          if(instance==null){
             instance = new Singleton();
          }     
        }
      }
      return instance;
    }
}

第六种是对第五种的改进,引入了volatile 关键字,上面的问题是由于语义顺序的问题造成的,为什么引入volatile关键字就可以避免呢?
volatile 修饰的变量不就是在多线程环境下可以做到内存的可见性吗?怎么就能解决上面的语义造成的问题呢?兄弟,告诉你,其实volatile
还用一个作用就是可以禁止指令指令重排序优化,是不是豁然开朗。但这只适用于JDK1.5以后,JDK1,5以前即使引入volatile 关键字也是不能保证线程安全的。

  • 第七种 (静态内部类,线程安全,强烈推荐)
public class Singleton{
    private Singleton(){}
    private  static class SingletonHolder{
         private static Singleton instance =new Singleton();
    }
    public static Singleton getSingleton(){
      return SingletonHolder.instance;
    }
}

优点:既能保证线程安全,又能实现懒加载(只有当使用到使用到SingletonHolder 这个类的时候才加载)。
缺点:没有想到(唯一能想到的就是可以利用java 反射机制强行访问构造器,和序列化与反序列化问题,后文有介绍)。

  • 第八种(使用枚举,强烈推荐)
public enum Singleton{
    /**
     * 定义一个枚举的元素,它就代表了Singleton的一个实例。
     */
    instance;
     /**
     * 单例可以有自己的操作
     */ 
    public void SingletonOperation(){
           //功能处理
    }
    }

调用的SingletonOperation可以这样 Singleton.instance.SingletonOperation()
优点:优雅,使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推 荐尽可能地使用枚举来实现单例
缺点:真没有想到

3.深入分析

上一节我们分析了各种写法的优缺点,但是没有考虑其他因素对代码的健壮性的影响,其实如果想深挖,还有一些因素对我们认为比较完美的单例有影响,这里主要从三个方面简单阐述下:
1. java 反射,由于Java反射机制可以访问到私有的构造函数,所以除了枚举这种方法,其他的所有的方法都失效了,解决办法:构造方法中抛异常 具体如何防 范,请参考如何防止JAVA反射对单例类的攻击
1. 当我们的单例需要序列化时,也会破坏我们的单例,解决办法:加一个readResolve方法 ,具体细节,请参考单例与序列化的那些事儿
1. 当我们使用多个类加载器是时,也会破坏我们的单例(主要针对的是饿汉式),具体细节请参考类加载器与单例
ps:java 平台中的单例模式有: java.lang.Runtime

4.总结

通过上面的讨论可知,有三种写法比较完美,那就是第六,七,八,既 双重检查锁机制 +volatile,静态内部类和枚举,其中枚举是最安全的。