也来谈谈懒汉和饿汉,详细解析单例模式的六种实现方式!

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战java

单例模式

  • 单例模式是一种建立模式,单例类负责本身建立本身的对象而且一个类只有一个实例对象,而且向整个系统提供这个实例.系统能够直接访问这个实例而不须要实例化
  • 单例模式的特色:
    • 单例类只有一个实例
    • 单例类必须本身建立自身的惟一实例
    • 单例类必须给其他系统对象提供建立的惟一实例

单例模式的实现方式

  • 单例模式要保证一个类只有一个实例,而且提供给全局访问,主要用于解决一个全局使用的类频繁建立和销毁的问题,经过判断系统是否存在这个单例来解决这样的问题,若是有这个单例则返回这个单例,不然就建立这个单例,只要保证构造函数是私有的便可
    • 保证一个类只有一个实例: 将该类的构造方法定义为私有方法便可
    • 提供全局一个该实例的访问点: 单例类本身建立实例,提供一个静态方法做为实例的访问点便可
  • 饿汉和懒汉比较:
    • 懒汉: 单例类对象实例懒加载,不会提早建立对象实例,只有在使用对象实例的时候才会建立对象实例
    • 饿汉: 在单例对象实例进行声明引用时就进行实例化建立对象实例
  • 单例模式除去线程不安全的懒汉,一般有五种实现方式:
    • 懒汉
    • 双检锁
    • 饿汉
    • 静态内部类
    • 枚举
  • 通常状况下,直接使用饿汉实现单例模式
  • 若是明确要求懒加载一般使用静态内部类实现单例模式
  • 若是有关于反序列化建立对象会考虑使用枚举实现单例模式
  • 静态类Static :
    • 静态类在第一次运行时直接初始化,也不须要在延迟加载中使用
    • 在不须要维持任何状态,仅仅用于全局访问时,使用静态类的方式更加方便
    • 若是须要被继承或者须要维持一些特定状态下的状况,就适合使用单例模式

线程不安全懒汉

线程安全懒汉

  • 单例模式线程安全懒汉Singleton示例
  • 解决了多线程环境下建立多个实例的问题
  • 存在每次获取实例都须要申请锁的问题,方法效率低下,由于在任什么时候候只能有一个线程能够调用getInstance() 方法

双检锁

  • 双重检查锁模式: doule checked locking pattern
    • 使用同步块加锁的方法
    • 会有两次检查instance == null
      • 一次在同步块外
      • 一次在同步块内
        • 由于会有多个线程一块儿进入同步块外的if
        • 若是不在同步块内不进行二次检验就会致使生成多个实例
  • 单例模式双检锁Singleton示例
  • volatile:
    • 对于计算机中的指令而言 ,CPU和编译器为了提高程序的执行效率,一般会按照必定的规则对指令进行优化
    • 若是两条指令互不依赖,那么指令执行的顺序可能不是源码的编写顺序
    • 形如instance = new Instance() 方法建立实例执行分为三步:
      • 分配对象内存空间: 给新建立的Instance对象分配内存
      • 初始化对象: 调用单例类的构造函数来初始化成员变量
      • 设置instance指向新建立的对象分配的内存地址,此时instance != null
        • 由于上面的初始化对象和设置instance指向新建立的对象分配的内存地址不存在数据上的依赖关系,不管哪一步先执行都不会影响最终结果,因此程序在编译时,顺序就会发生改变:
          • 分配对象内存空间
          • 设置instance指向新建立对象分配的内存地址
          • 初始化对象
        • CPU和编译器在指令重排时,不会关心指令重排执行是否影响多线程的执行结果. 若是不加volatile关键字,若是有多个线程访问getInstance() 方法时,若是恰好发生了指令重排,可能会出现如下状况:
          • 当第一个线程获取锁而且进入到第二个if方法后,先分配内存空间,而后instance指向刚刚分配的内存地址,此时instance不等于null. 可是此时instance尚未初始化完成
          • 若是此时有另外一个线程调用getInstance() 方法,在第一个if的判断时结果就为false, 就会直接返回没有初始化完成的instance, 这样可能会致使程序NPE异常
    • 使用volatile的缘由是禁止指令从新排序:
      • volatile变量进行赋值操做后会有一个内存隔离
      • 读操做不会重排序到内存隔离之中
      • 好比在上面操做中,读操做必须在执行完1,2,3或者1,3,2步骤以后才会执行读取到结果,不然不会读取到相关结果

饿汉

  • 单例模式饿汉Singleton示例
  • 优势:
    • 在单例类中,装载类的时候就建立对象实例.由于单例类的实例声明为staticfinal变量,在第一次加在类到内存中时就会初始化,因此建立实例自己时线程安全的
  • 缺点:
    • 饿汉模式不是一种懒加载模式,即使客户端没有调用getInstance() 方法,单例类也会在类第一次加载时初始化
    • 使用饿汉模式建立单例类实例在某些场景中没法使用:
      • 好比由于饿汉建立的实例声明为final变量
      • 若是单例类Singleton的实例的建立依赖参数或者配置文件
      • 须要在getInstance() 方法以前调用方法为单例类的实例设置参数,此时这种饿汉模式就没法使用

静态内部类

  • 单例模式静态内部类Singleton示例
  • 使用静态内部类模式建立单例类实例是使用JVM机制保证线程安全:
    • 静态单例对象没有做为单例类的成员变量直接实例化,因此当类加载时不会实例化单例类
    • 第一次调用getInstance() 方法时将加载静态内部类Nest. 在静态内部类中定义了一个static类型的变量instance, 这时会首先初始化这个变量
    • 经过JVM来保证线程安全,确保该成员变量只初始化一次
    • 因为getInstance() 方法并无加线程锁,因此对性能没有什么影响
  • 静态内部类的优势:
    • 静态内部类Nest是私有的,只能经过getInstance() 方法进行访问,因此这是懒加载的
    • 读取实例时不会进行同步锁的获取,性能较好
    • 静态内部类不依赖JDK版本

枚举

  • 单例模式枚举Singleton示例
  • 使用枚举方式实现单例的最大特色是很是简单
  • 能够经过Enum.INSTANCE来访问实例,和getInstance() 方法比较更加简单
  • 枚举的建立默认就是线程安全的方法,并且能防止反射以及反序列化致使从新建立新的对象
    • Enum类内部使用Enum类型断定防止经过反射建立新的对象
    • Enum类经过对象的类型和枚举名称将对象进行序列化,而后经过valueOf() 方法匹配枚举名称找到内存中的惟一对象实例,这样能够防止反序列化时建立新的对象
  • 懒汉式和饿汉式实现的单例模式破坏 : 不管是经过懒汉式仍是饿汉式实现的单例模式,均可能经过反射和反序列化破坏掉单例的特性,能够建立多个对象
  • 反射破坏单例模式: 利用反射,能够强制访问单例类的私有构造器,建立新的对象
public static void main(String[] args) {
	// 利用反射获取单例类的构造器
	Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
	// 设置访问私有构造器
 	constructor.setAccessiable(true);
 	// 利用反射建立新的对象
 	Singleton newInstance = constructor.newInstance();
 	// 经过单例模式建立单例对象
 	Singleton singletonInstance = Singleton.getInstance();
 	// 此时这两个对象是两个不一样的对象,返回false
 	System.out.println(singletonInstance  == newInstance);
}
复制代码
  • 反序列化破坏单例模式: 经过readObject() 方法读取对象时会返回一个新的对象实例
public static void main(String[] args) {
	// 建立一个输出流对象
	ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
	// 将单例类对象写入到文件中
	Singleton singletonInstance = Singleton.getInstance();
	os.writeObject(singleton);
	// 从文件中读取单例对象
	File file = new File("Singleton.file");
	ObjectInputStream is = new ObjectInputStream(new FileInputStream(file));
	Singleton newInstance = (Singleton)is.readObject();
	// 此时这两个对象是两个不一样的对象,返回false
	System.out.println(singletonInstance == newInstance);
}
复制代码
相关文章
相关标签/搜索