您的位置 首页 java

从简到难,一步步演进单例模式的五种写法

从简到难,一步步演进单例模式的五种写法

前言

有一些对象其实我们只需要一个,比方说:线程池,缓存,对话框,处理偏好设置和注册表的对象,日志对象,充当打印机,显卡等设备的驱动程序的对象。事实上,这类对象只能有一个实例,如果制造出多个实例,就会导致许多问题产生,例如:程序的行为异常,资源使用过量,或者是不一致的结果

涉及到一些类加载的知识,如果不清楚,可以看一下这篇分享: Java 类的加载顺序

单例模式确保一个类只有一个实例,并提供一个全局访问点,实现单例模式的方法是私有化构造函数,通过getInstance()方法 实例化 对象,并返回这个实例

实现

第一种(懒汉)

按照上面的想法,我们有了第一个实现

// code1
public class Singleton {

 private static Singleton uniqueInstance;

 private Singleton() {}

 public static Singleton getInstance() {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 return uniqueInstance;
 }

}
 

当2个 线程 同时进入getInstance()的if语句里面,会返回2个不同实例,因此这种方式是线程不安全的

// code2
public class Singleton {

 private static Singleton uniqueInstance;

 private Singleton() {}

 public static  synchronized  Singleton getInstance() {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 return uniqueInstance;
 }

}
 

用synchronized修饰可以保证 线程安全 ,但是只有第一次执行此方法时才需要同步,设置好 uniqueInstance,就不需要同步这个方法了,之后每次调用这个方法,同步都是一种累赘

第二种(双重检查锁定)

synchronized锁的粒度太大,人们就想到通过双重检查锁定来降低同步的开销,下面是实例代码

// code3
public class Singleton {

 private static Singleton uniqueInstance;

 private Singleton() {}

 public static Singleton getInstance() {
 if (uniqueInstance == null) {
 synchronized (Singleton.class) {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 }
 }
 return uniqueInstance;
 }

}
 

如上面代码所示,如果第一次检查uniqueInstance不为null,那么就不需要执行下面的加锁和初始化操作,可以大幅降低synchronized带来的性能开销,只有在多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象

有小伙伴对code3和cod4中,为什么要执行2次if语句不太清楚,简单描述一下,有可能有AB2个线程同时进入了第一个if语句,然后A拿到锁,创建对象完成。如果不再做一次判空处理的话,B拿到锁后会重新创建对象,加了第2个if语句,就直接退出了

双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到getInstance()方法的第4行,代码读取到uniqueInstance不为null时,uniqueInstance引用的对象有可能还没有完成初始化

简单概述一下《Java并发编程的艺术》的解释,

uniqueInstance = new Singleton()可以分解为如下三行 伪代码

memory = allocate(); // 1:分配对象的内存空间

ctorInstance(memory); // 2:初始化对象

uniqueInstance = memory;// 3:设置uniqueInstance指向刚分配的内存地址
 

3行伪代码中的2和3之间,可能会被重排序,重排序后执行时序如下

memory = allocate(); // 1:分配对象的内存空间

uniqueInstance = memory;// 3:设置uniqueInstance指向刚分配的内存地址
 // 注意,此时对象还没有被初始化

ctorInstance(memory); // 2:初始化对象
 

多个线程访问时可能出现如下情况

从简到难,一步步演进单例模式的五种写法

这样会导致线程B访问到一个还未初始化的对象,此时可以用volatile来修饰Singleton,这样3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止

// code4
public class Singleton {

 private volatile static Singleton uniqueInstance;

 private Singleton() {}

 public static Singleton getInstance() {
 if (uniqueInstance == null) {
 synchronized (Singleton.class) {
 if (uniqueInstance == null) {
 uniqueInstance = new Singleton();
 }
 }
 }
 return uniqueInstance;
 }

}
 

从简到难,一步步演进单例模式的五种写法

第三种(饿汉)

如果应用程序总是创建并使用单例式例,或者在创建和运行时方面的负担不太繁重,我们可以以饿汉式的方式来创建单例

// code5
public class Singleton {
 private static Singleton uniqueInstance = new Singleton();

 private Singleton() {}

 public static Singleton getInstance() {
 return uniqueInstance;
 }

}
 

// code6
public class Singleton {

 private static Singleton uniqueInstance;

 static {
 uniqueInstance = new Singleton();
 }

 private Singleton() {}
 public static Singleton getInstance() {
 return uniqueInstance;
 }

}
 

在类加载的时候直接创建这个对象,这样既能提高效率,又能保证线程安全,code5和code6几乎没有区别,因为静态成员变量和静态代码块都是类初始化的时候被加载

第四种(静态内部类)

// code7
public class Singleton {

 private static class SingletonHolder {
 private static Singleton uniqueInstance = new Singleton();
 }

 private Singleton() {}

 public static Singleton getInstance() {
 return SingletonHolder.uniqueInstance;
 }

}
 

饿汉式的方式只要Singleton类被装载了,那么uniqueInstance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,uniqueInstance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化uniqueInstance

第五种(枚举)

// code8
public enum Singleton { 
 INSTANCE; 
 public void whateverMethod() { 
 } 

}
 

公认的实现单例的最好方式,网上资料也比较少,还没有彻底理解清楚,说不定以后会补一篇文章说明,本篇文章中的code1和code3不建议使用,原因已经说明

从简到难,一步步演进单例模式的五种写法

文章来源:智云一二三科技

文章标题:从简到难,一步步演进单例模式的五种写法

文章地址:https://www.zhihuclub.com/196683.shtml

关于作者: 智云科技

热门文章

网站地图