vlambda博客
学习文章列表

史上最全单例模式的写法和破坏单例方式

今天跟大家讲一个老生常谈的话题,单例模式是最常用到的设计模式之一,熟悉设计模式的朋友对单例模式都不会陌生。

网上的文章也很多,但是参差不齐,良莠不齐,要么说的不到点子上,要么写的不完整,我试图写一篇史上最全单例模式,让你看一篇文章就够了。

 

单例模式定义及应用场景

单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

我们写单例的思路是,隐藏其所有构造方法,提供一个全局访问点。

1、饿汉式

这个很简单,小伙们都写过,这个在类加载的时候就立即初始化,因为他很饿嘛,一开始就给你创建一个对象,这个是绝对线程安全的,在线程还没出现以前就实例化了,不可能存在访问安全问题。他的缺点是如果不用,用不着,我都占着空间,造成内存浪费。

public classHungrySingleton{
private static final HungrySingleton hungrySingleton = new HungrySingleton();
privateHungrySingleton() { }
publicstatic HungrySingleton getInstance() { return hungrySingleton; }}

还有一种是饿汉式的变种,静态代码块写法,原理也是一样,只要是静态的,在类加载的时候就已经成功初始化了,这个和上面的比起来没什么区别,无非就是装个b,看起来比上面那种吊,因为见过的人不多嘛。

public classHungryStaticSingleton{
private static final HungryStaticSingleton hungrySingleton;
static { hungrySingleton = new HungryStaticSingleton(); }
privateHungryStaticSingleton() { }
publicstatic HungryStaticSingleton getInstance() { return hungrySingleton; }
}


2、懒汉式

简单懒汉

为了解决饿汉式占着茅坑不拉屎的问题,就产生了下面这种简单懒汉式的写法,一开始我先申明个对象,但是先不创建他,当用到的时候判断一下是否为空,如果为空我就创建一个对象返回,如果不为空则直接返回。

为什么叫懒汉式,就是因为他很懒啊,要等用到的时候才去创建,看上去很ok,但是在多线程的情况下会产生线程安全问题。

public class LazySimpleSingleton {
private static LazySimpleSingleton instance;
privateLazySimpleSingleton() { }
publicstatic LazySimpleSingleton getInstance() { if (instance == null) { instance = new LazySimpleSingleton(); } return instance; }
}

如果有两个线程同时执行到 if (instance==null) 这行代码,这是判断都会通过,然后各自会执行instance = new Singleton(),并各自返回一个instance,这时候就产生了多个实例,就没有保证单例,如下图所示。

史上最全单例模式的写法和破坏单例方式

怎么解决这个问题呢,很简单,加锁啊,加一下synchronized即可,这样就能保住线程安全问题了。

史上最全单例模式的写法和破坏单例方式


3、双重校验锁(DCL)

上面这样写法带来一个缺点,就是性能低,只有在第一次进行初始化的时候才需要进行并发控制,而后面进来的请求不需要在控制了,现在synchronized加在方法上,我管你生成没成生成,只要来了就得给我排队,所以这种性能是极其低下的,那怎么办呢?

我们知道,其实synchronized除了加在方法上,还可以加在代码块上,只要对生成对象的那一部分代码加锁就可以了,由此产生一种新的写法,叫做双重检验锁,我们看下面代码。

史上最全单例模式的写法和破坏单例方式

我们看19行将synchronized包在了代码块上,当 singleton == null 的时候,我们只对创建对象这一块逻辑进行了加锁控制,如果 singleton != null 的话,就直接返回,大大提升了效率。

在21行的时候又加了一个singleton == null,这又是为什么呢,原因是如果两个线程都到了18行,发现是空的,然后都进入到代码块,这里虽然加了synchronized,但作用只是进行one by one串行化,第一个线程往下走创建了对象,第二个线程等待第一个线程执行完毕后,我也往下走,于是乎又创建了一个对象,那还是没控制住单例,所以在21行当第二个线程往下走的时候在判断一次,是不是被别的线程已经创建过了,这个就是双重校验锁,进行了两次非空判断。

我们看到在11行的时候加了 volatile 关键字,这是用来防止指令重排的,当我们创建对象的时候会经过下面几个步骤,但是这几个步骤不是原子的,计算机比较聪明,有时候为了提高效率他不是按顺序1234执行的,可能是3214执行。

  • 分配内存给这个对象
  • 初始化对象
  • 设置instance指向刚分配的内存地址
  • 初次访问对象

最后说下双重校验锁,虽然提高了性能,但是在我看来不够优雅,折腾来折腾去,一会防这一会防那,尤其是对新手不友好,新手会不明白为什么要这么写。


4、静态内部类

上面已经将锁的粒度缩小到创建对象的时候了,但不管加在方法上还是加在代码块上,终究还是用到了锁,只要用到锁就会产生性能问题,那有没有不用锁的方式呢?

答案是有的,那就是静态内部类的方式,他其实是利用了java代码的一种特性,静态内部类在主类加载的时候是不会被加载的,只有当调用getInstance()方法的时候才会被加载进来进行初始化,代码如下

/** * @author jack xu * 兼顾饿汉式的内存浪费,也兼顾synchronized性能问题 */public classLazyInnerClassSingleton{
privateLazyInnerClassSingleton() { }
publicstaticfinal LazyInnerClassSingleton getInstance() { return LazyHolder.INSTANCE; }
private static classLazyHolder{ private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton(); }
}

好,讲到这里我已经介绍了五种单例的写法,经过层层的演进推理,到第五种的时候已经是很完美的写法了,既兼顾饿汉式的内存浪费,也兼顾synchronized性能问题。

那他真的一定完美吗,其实不然,他还有一个安全的问题,接下来我们讲下单例的破坏,有两种方式反射和序列化。

 

单例的破坏

1、反射

我们知道在上面单例的写法中,在构造方法上加上private关键字修饰,就是为了不让外部通过new的方式来创建对象,但还有一种暴力的方法,我就是不走寻常路,你不让我new是吧,我反射给你创建出来,代码如下

/** * @author jack xu */public classReflectDestroyTest{
public static void main(String[] args) { try { Class<?> clazz = LazyInnerClassSingleton.class; Constructor c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Object o1 = c.newInstance(); Object o2 = c.newInstance(); System.out.println(o1 == o2); } catch (Exception e) { e.printStackTrace(); } }}

史上最全单例模式的写法和破坏单例方式

那么如何防止反射呢,很简单,就是在构造方法中加一个判断

public classLazyInnerClassSingleton{
privateLazyInnerClassSingleton() { if (LazyHolder.INSTANCE != null) { throw new RuntimeException("不要试图用反射破坏单例模式"); } }
publicstaticfinal LazyInnerClassSingleton getInstance() { return LazyHolder.INSTANCE; }
private static classLazyHolder{ private static final LazyInnerClassSingleton INSTANCE = new LazyInnerClassSingleton(); }}

在看结果,防止反射成功,当调用构造方法时,发现单例实例对象已经不为空了,抛出异常,不让你在继续创建了。

史上最全单例模式的写法和破坏单例方式


2、序列化

接下来介绍单例的另一种破坏方式,先在静态内部类上实现Serializable接口,然后写个测试方法测试下,先创建一个对象,然后把这个对象先序列化,然后在反序列化出来,然后对比一下

 publicstaticvoidmain(String[] args) {
LazyInnerClassSingleton s1 = null; LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null; try {
fos = new FileOutputStream("SeriableSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (LazyInnerClassSingleton) ois.readObject(); ois.close();
System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2);
} catch (Exception e) { e.printStackTrace(); } }

我们来看结果发现是false,在进行反序列化时,在ObjectInputStream的readObject生成对象的过程中,其实会通过反射的方式调用无参构造方法新建一个对象,所以反序列化后的对象和手动创建的对象是不一致的。

史上最全单例模式的写法和破坏单例方式

那么怎么避免呢,依然很简单,在静态内部类里加一个readResolve方法即可

 private Object readResolve() { return LazyHolder.INSTANCE; }

在看结果就变成true了,为什么加了一个方法就可以避免被序列化破坏呢,这里不在展开,感兴趣的小伙伴可以看下ObjectInputStream的readObject()方法,一步步往下走,会发现最终会调用readResolve()方法。

史上最全单例模式的写法和破坏单例方式

至此,史上最牛b单例产生,已经无懈可击、无可挑剔了。


3、枚举

那么这里我为什么还要在介绍枚举呢,在《Effective Java》中,枚举是被推荐的一种方式,因为他足够简单,线程安全,也不会被反射和序列化破坏。

大家看下才寥寥几句话,不像上面虽然已经实现了最牛b的写法,但是其中的过程很让人烦恼啊,要考虑性能、内存、线程安全、破坏啊,一会这里加代码一会那里加代码,才能达到最终的效果。

而使用枚举,感兴趣的小伙伴可以反编译看下,枚举的底层其实还是一个class类,而我们考虑的这些问题JDK源码其实帮我们都已经实现好了,所以在 java 层面我们只需要用三句话就能搞定!

public enum Singleton { INSTANCE; publicvoidwhateverMethod() { }}

至此,我通过层层演进,由浅入深的给大家介绍了单例的这么多写法,从不完美到完美,这么多也是网上很常见的写法,下面我在送大家两个彩蛋,扩展一下其他写单例的方式方法。

 

彩蛋

1、容器式单例

容器式单例是我们 spring 中管理单例的模式,我们平时在项目中会创建很多的Bean,当项目启动的时候spring会给我们管理,帮我们加载到容器中,他的思路方式方法如下。

public classContainerSingleton{ private ContainerSingleton() { }
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) { Object instance = null; if (!ioc.containsKey(className)) { try { instance = Class.forName(className).newInstance(); ioc.put(className, instance); } catch (Exception e) { e.printStackTrace(); } return instance; } else { return ioc.get(className); } }}

这个可以说是一个简易版的 spring 管理容器,大家看下这里用一个map来保存对象,当对象存在的时候直接从map里取出来返回出去,如果不存在先用反射创建一个对象出来,先保存到map中然后在返回出去。

我们来测试一下,先创建一个Pojo对象,然后两次从容器中去取出来,比较一下,发现结果是true,证明两次取出的对象是同一个对象。

史上最全单例模式的写法和破坏单例方式

但是这里有一个问题,这样的写法是线程不安全的,那么如何做到线程安全呢,这个留给小伙伴自行独立思考完成。


2、CAS单例

从一道面试题开始:不使用synchronized和lock,如何实现一个线程安全的单例?我们知道,上面讲过的所有方式中,只要是线程安全的,其实都直接或者间接用到了synchronized,间接用到是什么意思呢,就比如饿汉式、静态内部类、枚举,其实现原理都是利用借助了类加载的时候初始化单例,即借助了ClassLoader的线程安全机制。

所谓ClassLoader的线程安全机制,就是ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的,也就是保证了线程安全。

那么答案是什么呢,就是利用CAS乐观锁,他虽然名字中有个锁字,但其实是无锁化技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试,代码如下:

/** * @author jack xu */public class CASSingleton { private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();
privateCASSingleton() { }
publicstatic CASSingleton getInstance() { for (; ; ) { CASSingleton singleton = INSTANCE.get(); if (null != singleton) { return singleton; }
singleton = new CASSingleton(); if (INSTANCE.compareAndSet(null, singleton)) { return singleton; } } }}

在JDK1.5中新增的JUC包就是建立在CAS之上的,相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现,他是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

虽然CAS没有用到锁,但是他在不停的自旋,会对CPU造成较大的执行开销,在生产中我们不建议使用,那么为什么我还会讲呢,因为这是工作拧螺丝,面试造火箭的典型!你可以不用,但是你得知道,你说是吧。

- END -

原文链接:

jack_xu
https://juejin.im/post/5ed5c50af265da76d3187b30

文源网络,仅供学习之用。如有侵权,联系删除

史上最全单例模式的写法和破坏单例方式


  往期好文  


◆  

◆  

◆  

◆  

◆  

◆  

◆  

◆  

◆  

◆  



史上最全单例模式的写法和破坏单例方式



关注下方二维码

每天推送优质文章

你会有意想不到的收获