创建型设计模式之单例模式的十种写法


单例模式在设计模式里面应该算是很简单的一种,一直以来都以为它的写法很简单,没有太大的难度,但是在看了一些大牛的视频和书籍讲解之后,发现自己其实在单例模式方面算是个小白,因为单例模式的写法居然有达十种之多,简直不可思议。
下面我们一起来聊一聊单例模式的这几种写法:

饿汉模式

饿汉模式和懒汉模式是初学设计模式时接触到的,当初以为就这两种写法,下面我们来看看饿汉模式。
代码如下:

public class HungaryDesignPattern {
    private static final HungaryDesignPattern hungary = new HungaryDesignPattern();
    private HungaryDesignPattern(){
        System.out.println("=============初始化============");
    }

    public static HungaryDesignPattern getInstance(){
        return hungary;
    }

    public static void main(String[] args) {

    }
}

运行结果:

=============初始化============

饿汉模式就是这么简单,当类加载器对其加载后,类加载的准备阶段(因为static关键字)就会对其进行初始化,所以在这里main方法即使没有调用任何的初始化方法,仍然可以打印出构造方法的字符串。这样再通过getInstance()获取到的对象就是已经初始化好了的,可以直接使用。

这种模式,简单、明了,易于使用,也是线程安全的;

懒汉模式

基础懒汉模式

懒汉模式跟饿汉模式稍有不同,并不是一开始就完成初始化,而是当有其他线程要使用到它的实例时,才会去进行初始化,代码如下:

public class LazyDesignPattern_01 {
    private static LazyDesignPattern_01 lazy=null;
    private LazyDesignPattern_01(){}

    public static LazyDesignPattern_01 getInstance(){
        if(null==lazy){
            lazy = new LazyDesignPattern_01();
        }
        return lazy;
    }
}

这就懒汉模式的基础版本,有开发经验的人一眼就看出症结所在,这个单例并不是线程安全的。先分析下为什么不是线程安全的,比如现在有两个线程A和B,当A线程进入了getInstance()方法,且已经判断了lazy没有初始化,正准备对其进行初始化(初始化操作并不是原子操作),此时B线程也进来,也判断到lazy对象没有初始化,B线程也会对其进行初始化,就会导致两次初始化操作,后初始化会覆盖先初始化的数据。从而造成线程不安全问题。

既然这样不安全,那我对其加锁吧,也就是懒汉模式的另一种写法:

加锁懒汉模式

上面说了,第一种写法是不安全的,那我加上锁吧,代码如下:

public class LazyDesignPattern_02 {
    private static LazyDesignPattern_02 lazy=null;
    private LazyDesignPattern_02(){}

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

这种写法的意图很明确,当我检测到对象没有初始化时,当前线程优先获取LazyDesignPattern_02 类的Class对象的锁,然后在同步块中对其进行初始化,这样应该就保证了线程的安全了吧。

想法是正确的,但是分析代码来看仍然还是有问题滴,首先在判断没有初始化的时候,实际上两个线程都有可能判断到对象没有被初始化,这时候A、B线程中的A先获取到了锁,B就只有阻塞,等待A释放锁,然后B进入同步块中,还是要进行初始化,最终覆盖A线程初始化的数据,所以这种写法仍然不是线程安全的。

那我们来继续完善下懒汉式的写法:

双重检测懒汉模式

这里不多说,先看代码:

public class LazyDesignPattern_03 {
    private static LazyDesignPattern_03 lazy=null;
    private LazyDesignPattern_03(){}

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

该代码跟前面的代码没有太大区别,只是在同步块中再一次做了是否初始化的判断检测,所以这种方式称作双重检测。
这种写法的目的是建立在上一种写法上,上面已经说了A、B线程仍然有可能都去初始化对象,那我在同步块中再做一次判断不就可以了吗,这样可以在A初始完成后,B进入同步块发现对象已经初始化,那就不会再去进行初始化操作了,这样总可以避免上一种写法的问题了吧。

这里就需要更深入的分析虚拟机的操作指令了,虽然lazy = new LazyDesignPattern_03(),在代码中只有一行,但实际上再jvm中编译的指令并不只是一条,一起来看看下图中这部分代码的编译后的jvm指令:
在这里插入图片描述
对照上图,synchronized关键字编译后生成的字节码指令 会有一个“ monitorenter”和“ monitorexit”,在这两个指令之间的就是同步块, “if_acmpn”表示的是if判断语句,“new”指令才是初始化对象的开始,到初始化结束(monitorexit),中间还有4条指令。
当初始化完成后赋值给lazy,虽然A线程在工作内存中完成了赋值,但还要将赋值结果同步到主内存中,在这个过程中B线程很可能获取到的并不是赋值后的lazy,那么后续的if判断和同步块也会执行,也会出现非线程安全的情况。

那到底应该怎样才能让懒汉模式的单例能够保证线程安全呢,我们继续往下看:

双重检测+Volatile懒汉模式

直接看代码:

public class LazyDesignPattern_04 {
    private static volatile LazyDesignPattern_04 lazy=null;
    static{
        System.out.println("=======================");
    }

    private LazyDesignPattern_04(){
        System.out.println("============初始化==========");
    }

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

    public static void main(String[] args) {
        LazyDesignPattern_04.getInstance();
    }
}

当前代码跟上面代码相比只是在lazy对象上添加了volatile关键字修饰,怎么就能够保证线程的安全了呢,这个要从volatile关键字的线程可见性讲起,简单来讲,就是A线程对lazy赋值后,B线程立即可见,那么在双重检测的情况下,就可以检测到lazy已经初始完毕,就不会再次进行初始化。
而且该关键字修饰的共享遍量,写操作会优先于读操作,就是说A线程在赋值成功后,立即同步到主线程,而B线程已经获取到的lazy副本已经失效,在判断时,必须重新读取主内存的中lazy,这样就保证了在后续的为空判断中不会成功,直接返回lazy对象。

最后这一种写法在多线程情况下可以保证安全了,已经是可以正常用作开发的了。后面我们在讲几种非正常情况初始化,从而导致单例模式遭到破坏的情况

破坏单例模式

反射破坏单例模式,以及修复

先来看一段代码:

public class BrokenLazyDesignPattern_01 {
    private static volatile BrokenLazyDesignPattern_01 lazy=null;

    private BrokenLazyDesignPattern_01(){}

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

    public static void main(String[] args)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        BrokenLazyDesignPattern_01 instance = BrokenLazyDesignPattern_01.getInstance();

        Object o = Class.forName("com.fq.thread.design.BrokenLazyDesignPattern_01").newInstance();
        BrokenLazyDesignPattern_01 lazy = (BrokenLazyDesignPattern_01)o;

        System.out.println("instance.hashCode="+instance.hashCode());
        System.out.println("lazy.hashCode="+lazy.hashCode());
    }
}

打印结果:

instance.hashCode=356573597
lazy.hashCode=1735600054

由以上结果可知,直接调用getInstance()返回的对象,跟通过Class.forName反射得出的对象,并不是同一个对象,因此单例遭到破坏。那么怎么让反射的时候初始化的对象,跟直接调用getInstance()返回的对象是同一个呢,这里有两种写法:

静态内部类防止单例破坏

先看一段静态内部类的代码:

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton(){
        System.out.println("=======StaticInnerClassSingleton========");
        if(null!=SingletonHolder.holder){
            throw new RuntimeException("不能通过反射初始化该对象");
        }
    }

    public static StaticInnerClassSingleton getInstance(){
        return SingletonHolder.holder;
    }

    private static class SingletonHolder{
        static{
            System.out.println("===========SingletonHolder==========");
        }
        private static final StaticInnerClassSingleton holder = new StaticInnerClassSingleton();
    }


    public static void main(String[] args)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        StaticInnerClassSingleton instance = StaticInnerClassSingleton.getInstance();

        Object o = Class.forName("com.fq.thread.design.StaticInnerClassSingleton").newInstance();
        StaticInnerClassSingleton lazy = (StaticInnerClassSingleton)o;

        System.out.println("instance.hashCode="+instance.hashCode());
        System.out.println("lazy.hashCode="+lazy.hashCode());
    }

输出结果:

===========SingletonHolder==========
=======StaticInnerClassSingleton========
=======StaticInnerClassSingleton========
Exception in thread "main" java.lang.RuntimeException: 不能通过反射初始化该对象
	at com.fq.thread.design.StaticInnerClassSingleton.<init>(StaticInnerClassSingleton.java:13)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.lang.Class.newInstance(Class.java:442)
	at com.fq.thread.design.StaticInnerClassSingleton.main(StaticInnerClassSingleton.java:33)

这样就杜绝了反射初始化的意图,那到底是怎么解决的呢?
首先我们需要知道,同一个类加载器,对同一个类只会加载一次,而SingletonHolder类属于内部类,不能被外部访问,所以一开始并不会加载;
当调用getInstance()方法时,这里调用了内部类的静态常量holder,此时会触发SingletonHolder的加载,同时也会触发StaticInnerClassSingleton的初始化;
在StaticInnerClassSingleton的构造方法中null!=SingletonHolder.holder的判断是false,所以就完成了StaticInnerClassSingleton类的初始化;
此时Class.forName想通过反射newInstance()调用无参构造函数时,null!=SingletonHolder.holder的判断结果为true(因为SingletonHolder.holder属于类常量,且不能再次修改),返回异常。

枚举类防止单例破坏

通过枚举的方式来防止单例遭到破坏,看代码:

public enum EnumerationSingleton {
    
    INSTANCE;
    private int count=0;
    
    private EnumerationSingleton(){
       count=8;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        EnumerationSingleton instance = EnumerationSingleton.INSTANCE;
        System.out.println("count="+instance.getCount());
        
        Object o = Class.forName("com.fq.thread.design.EnumerationSingleton").newInstance();
        EnumerationSingleton lazy = (EnumerationSingleton)o;
        
        System.out.println("instance.hashCode="+instance.hashCode());
        System.out.println("lazy.hashCode="+lazy.hashCode());
    }
}

运行结果:

count=8
Exception in thread "main" java.lang.InstantiationException: com.fq.thread.design.EnumerationSingleton
	at java.lang.Class.newInstance(Class.java:427)
	at com.fq.thread.design.EnumerationSingleton.main(EnumerationSingleton.java:27)
Caused by: java.lang.NoSuchMethodException: com.fq.thread.design.EnumerationSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.newInstance(Class.java:412)
	... 1 more

由此可以看出枚举类是线程安全的,即使反射也不能成功,因为枚举类没有构造函数,所以通过反射来获取会导致异常。

反序列化破坏单例模式

看下面的代码:

public class SerializableBrokenSingleton implements Serializable {

    private static final SerializableBrokenSingleton singleton = new SerializableBrokenSingleton();
    public int count = 1;
    private SerializableBrokenSingleton(){
        System.out.println("=============初始化============");
    }

    public static SerializableBrokenSingleton getInstance(){
        return singleton;
    }

    public static void main(String[] args){
        ObjectOutputStream objectOutputStream = null;
        ObjectInputStream objectInputStream = null;
        try {
            SerializableBrokenSingleton instance = SerializableBrokenSingleton.getInstance();
            objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.obj"));
            objectOutputStream.writeObject(instance);
            objectOutputStream.flush();

            objectInputStream = new ObjectInputStream(new FileInputStream("out.obj"));
            Object o = objectInputStream.readObject();
            SerializableBrokenSingleton brokenSingleton = (SerializableBrokenSingleton)o;

            System.out.println("instance.hashCode="+instance.hashCode());
            System.out.println("brokenSingleton.hashCode="+brokenSingleton.hashCode());
            objectInputStream.close();
        }catch (Exception e){

        }
    }
}

打印结果:

=============初始化============
instance.hashCode=1735600054
brokenSingleton.hashCode=21685669

有打印结果可以看到反序列化之后的对象跟序列化之前的对象hashCode并不相同,所以对象并不相等。那有什么办法解决呢?只需要在类中添加一个方法即可,看代码:

public class SerializableBrokenSingleton implements Serializable {

    private static final SerializableBrokenSingleton singleton = new SerializableBrokenSingleton();
    public int count = 1;
    private SerializableBrokenSingleton(){
        System.out.println("=============初始化============");
    }

    public static SerializableBrokenSingleton getInstance(){
        return singleton;
    }

    private Object readResolve(){
        return singleton;
    }

    public static void main(String[] args){
        ObjectOutputStream objectOutputStream = null;
        ObjectInputStream objectInputStream = null;
        try {
            SerializableBrokenSingleton instance = SerializableBrokenSingleton.getInstance();
            objectOutputStream = new ObjectOutputStream(new FileOutputStream("out.obj"));
            objectOutputStream.writeObject(instance);
            objectOutputStream.flush();

            objectInputStream = new ObjectInputStream(new FileInputStream("out.obj"));
            Object o = objectInputStream.readObject();
            SerializableBrokenSingleton brokenSingleton = (SerializableBrokenSingleton)o;

            System.out.println("instance.hashCode="+instance.hashCode());
            System.out.println("brokenSingleton.hashCode="+brokenSingleton.hashCode());
            objectInputStream.close();
        }catch (Exception e){

        }
    }
}

打印结果:

=============初始化============
instance.hashCode=1735600054
brokenSingleton.hashCode=1735600054

此时序列化前后对象的hashCode都是相当,表示对象都是相同的。那为什么添加了一个readResolve()方法并返回instance之后,对象就一样了呢 ,我们一起来分析下ObjectOutputStream,ObjectInputStream的代码:
先看ObjectInputStream的readObject:

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }
        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
           Object obj = readObject0(false);
            ...
        } finally {
            ...
        }
    }
    
private Object readObject0(boolean unshared) throws IOException {
      ...
        depth++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();
                case TC_REFERENCE:
                    return readHandle(unshared);
                case TC_CLASS:
                    return readClass(unshared);
                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);
                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));
                case TC_ARRAY:
                    return checkResolve(readArray(unshared));
                case TC_ENUM:
                    return checkResolve(readEnum(unshared));
                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
                case TC_EXCEPTION:
                    ...
                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                 ...
                case TC_ENDBLOCKDATA:
				...
            }
        ...
    }
    
private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        ...
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
            {
            Object rep = desc.invokeReadResolve(obj);
           ...
    }

由以上代码我们可以知道,在调用desc.hasReadResolveMethod()时,调用的过程:首先是调用readObject0(),然后在里面判断类型为Object时,调用desc.hasReadResolveMethod()方法,再在该方法中判断desc.hasReadResolveMethod()(即是否有readResolve方法),如果有则通过** desc.invokeReadResolve(obj)**调用该方法,并返回对象。那么是在什么时候对hasReadResolveMethod属性赋值的呢,那就要看看ObjectOutputStream的writeObject():

public final void writeObject(Object obj) throws IOException {
       ..
            writeObject0(obj, false);
      ...
    }
 private void writeObject0(Object obj, boolean unshared)
        throws IOException
    {
             ...
              desc = ObjectStreamClass.lookup(cl, true);
             ...
      }
//ObjectStreamClass 类中的方法
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
       ...
                entry = new ObjectStreamClass(cl);
           ...
    }
//调用构造方法
private ObjectStreamClass(final Class<?> cl) {
        ...
                    if (externalizable) {
                        cons = getExternalizableConstructor(cl);
                    } else {
                        cons = getSerializableConstructor(cl);
                        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                            new Class<?>[] { ObjectOutputStream.class },
                            Void.TYPE);
                        readObjectMethod = getPrivateMethod(cl, "readObject",
                            new Class<?>[] { ObjectInputStream.class },
                            Void.TYPE);
                        readObjectNoDataMethod = getPrivateMethod(
                            cl, "readObjectNoData", null, Void.TYPE);
                        hasWriteObjectData = (writeObjectMethod != null);
                    }
                    writeReplaceMethod = getInheritableMethod(
                        cl, "writeReplace", null, Object.class);
                    readResolveMethod = getInheritableMethod(
                        cl, "readResolve", null, Object.class);
                 ...
    }

所以最终得出结果,在将对象写入到文件的时候,生成ObjectStreamClass的时候,就会判断是否存在私有的readResolve方法,这样在反序列化的时候就可以调用该方法返回指定的对象。

容器单例模式

public class ContainerSingleton {
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();
    private ContainerSingleton(){}

    public static Object getBean(String className){
        synchronized (ioc){
            if(!ioc.containsKey(className)){
                Object obj = null;
                try {
                    Object o = Class.forName(className).newInstance();
                    ioc.put(className, o);
                }catch (Exception e){

                }
                return obj;
            }else {
                return ioc.get(className);
            }
        }
    }
}

这种是模仿spring的ioc容器来实现的单例,类似于懒汉模式基础版本,也存在线程安全的问题,具体改进方式可以参考懒汉模式。

线程单例模式

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue() {
            return new ThreadLocalSingleton();
        }
    };
    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocal.get();
    }
}

上面这种写法是通过ThreadLocal来实现,ThreadLocal表示线程独有,因此会在每个线程中都会生成一份独立的ThreadLocal对象,线程之间互不影响,所以是绝对线程安全的。

以上就是总结的单例模式的多种写法,有不正确的地方还希望广大网友指正。。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值