单例模式的几种实现方式
Posted Java高知社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的几种实现方式相关的知识,希望对你有一定的参考价值。
1.单例模式简述
单例模式顾名思义就是"一个实例"的设计模式,需要使用设计模式的思路达成调用类的时候都是同一个实例。那么自然的构造函数肯定不能随意调用了,那肯定是私有的。但是肯定要有接口让外部调用啊,所以得提供一个接口,这个接口呢,返回的每一个实例都应该是一样的。
2.单例模式的主要实现方式
单例模式刚接触的时候以为只有懒汉式和饥汉式,随着了解的逐步深入,了解了更多优雅更全面的写法,需要兼顾线程安全。
2.1 饥汉式(饿汉式)
package com.example.test.demo; public class JHanSingleton { //static final单例对象,类加载的时候就初始化 private static final JHanSingleton instance = new JHanSingleton(); //私有构造方法,使得外界不能直接new private JHanSingleton() { } //公有静态方法,对外提供获取单例接口 public static JHanSingleton getInstance() { return instance; } } |
饥汉式解决了多线程并发的问题,因为在加载这个类的时候,就实例化了instance。当getInstatnce方法被调用时,得到的永远是类加载时初始化的对象(反序列化的情况除外)。但这也带来了另一个问题,如果有大量的类都采用了饥汉式,那么在类加载的阶段,会初始化很多暂时还没有用到的对象,这样肯定会浪费内存,影响性能。
2.2 懒汉式
package com.example.test.demo; public class LanHanSingleton { private static LanHanSingleton instance; private LanHanSingleton() { } /** * 增加synchronized关键字,该方法为同步方法,保证多线程单例对象唯一 */ public static synchronized LanHanSingleton getInstance() { if (instance == null) { instance = new LanHanSingleton(); } return instance; } } |
可以注意到getInstance方法前加了synchronized 关键字,让getInstance方法成为同步方法,这样就保证了当getInstance第一次被调用,即instance被实例化时,别的调用不会进入到该方法,保证了多线程中单例对象的唯一性。
优点:单例对象在第一次调用才被实例化,有效节省内存,并保证了线程安全。
缺点:同步是针对方法的,以后每次调用getInstance时(就算intance已经被实例化了),也会进行同步,造成了不必要的同步开销。
2.3 Double CheckLock(DCL)单例模式
package com.example.test.demo; public class DCLSingleton { //增加volatile关键字,确保实例化instance时,编译成汇编指令的执行顺序 private volatile static DCLSingleton instance; private DCLSingleton() { } public static DCLSingleton getInstance() { if (instance == null) { synchronized (DCLSingleton.class) { //当第一次调用getInstance方法时,即instance为空时,同步操作,保证多线程实例唯一 //当以后调用getInstance方法时,即instance不为空时,不进入同步代码块,减少了不必要的同步开销 if (instance == null) { instance = new DCLSingleton(); } } } return instance; } } |
如果instance不加volatile的话DCL失效:
在JDK1.5之前,可能会有DCL实现的问题,上述代码中的如下代码,在Java里虽然是一句代码,但它并不是一个真正的原子操作。
instance = new DCLSingleton(); |
它编译成最终的汇编指令,会有下面3个阶段:
给DCLSingleton实例分配内存
调用DCLSingleton的构造函数,初始化成员变量。
将instance指向分配的内存空间(这个操作以后,instance才不为null)
在jdk1.5之前,上述的2、3步骤不能保证顺序,也就是说有可能是1-2-3,也有可能是1-3-2。如果是1-3-2,当线程A执行完步骤3(instance已经不为null),但是还没执行完2,线程B又调用了getInstance方法,这时候线程B所得到的就是线程A没有执行步骤2(没有执行完构造函数)的instance,线程B在使用这样的instance时,就有可能会出错。这就是DCL失效。也叫指令重排。
知识点:什么是指令重排?
简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。
在jdk1.5之后,可以使用volatile关键字,保证汇编指令的执行顺序,虽然会影响性能,但是和程序的正确性比起来,可以忽悠不计。这里用一句话概况就是volatile会禁止指令重排。(volatile另一个作用是保证可见性)
优点:第一次执行getInstance时instance才被实例化,节省内存;多线程情况下,基本安全;并且在instance实例化以后,再次调用getInstance时,不会有同步消耗。
缺点:jdk1.5以前,有可能DCL失效,不过现在一般都会用比较高的版本,这个问题不大;Java内存模型影响导致失效;jdk1.5以后,使用volatile关键字,虽然能解决DCL失效问题,但是会影响部分性能。
2.4 静态内部类单例模式
package com.example.test.demo; public class StaticClassSingleton { //私有的构造方法,防止new private StaticClassSingleton() { } public static StaticClassSingleton getInstance() { return StaticClassSingletonHolder.instance; } /** * 静态内部类 */ private static class StaticClassSingletonHolder { //第一次加载内部类的时候,实例化单例对象 private static final StaticClassSingleton instance = new StaticClassSingleton(); } } |
第一次加载StaticClassSingleton类时,并不会实例化instance,只有第一次调用getInstance方法时,Java虚拟机才会去加载StaticClassSingletonHolder类,继而实例化instance,这样延时实例化instance,节省了内存,并且也是线程安全的。这是推荐使用的一种单例模式。
这个我不是很明白,为什么静态内部类里面的实例变量不会被加载,我写个demo验证一下正确性,至于为什么估计涉及到JVM 类的加载=-=
demo:
package com.example.test.demo; public class StaticClassSingleton { //私有的构造方法,防止new private StaticClassSingleton() { } public static StaticClassSingleton getInstance() { System.out.println("StaticClassSingleton getInstance"); return StaticClassSingletonHolder.instance; } public static void test(){ System.out.println("StaticClassSingleton test"); } /** * 静态内部类 */ private static class StaticClassSingletonHolder { //第一次加载内部类的时候,实例化单例对象 private static final StaticClassSingleton instance = new StaticClassSingleton(); static { System.out.println("StaticClassSingletonHolder load static field"); } } } |
package com.example.test; import com.example.test.demo.StaticClassSingleton; public class SingletonTest { public static void main(String[] args){ StaticClassSingleton.test(); StaticClassSingleton.getInstance(); } } |
执行结果:
StaticClassSingleton test StaticClassSingleton getInstance StaticClassSingletonHolder load static field Process finished with exit code 0 |
恩,从结果来看是对的。我这个demo的依据是静态代码块和静态成员变量都只会加载一次,所以如果是紧接着getInstance后面加载那上面说的没错。相应的如果我多调用一个getInstance,那static代码块也不会再次加载了。
StaticClassSingleton test StaticClassSingleton getInstance StaticClassSingletonHolder load static field StaticClassSingleton getInstance |
2.5 枚举单例模式
package com.example.demo_11_singleton.demo; public enum EnumSingleton { //枚举实例的创建是线程安全的,任何情况下都是单例(包括反序列化) INSTANCE; public void doSomething(){ } } |
枚举不仅有字段还能有自己的方法,并且枚举实例创建是线程安全的,就算反序列化时,也不会创建新的实例。
----------
除了枚举模式以外,其他实现方式,在反序列化时都会创建新的对象。
为了防止对象在反序列化时创建新的对象,需要加上如下方法:
private Object readResole() throws ObjectStreamException { return instance; } |
这是一个钩子函数,在反序列化创建对象时会调用它,我们直接返回instance就是说,不要按照默认那样去创建新的对象,而是直接将instance返回。
3.总结
看了这么多写法,其实懒汉式和饥汉式已经被后面更优雅的实现方式比下去了,看起来用double check、静态内部类或者枚举会更好一点。
长按二维码识别关注Java高知社区,学习更多高知知识,点赞+转发
以上是关于单例模式的几种实现方式的主要内容,如果未能解决你的问题,请参考以下文章