单例模式深入解析(Java)

前言

相信各位对单例模式都不陌生,这已经是一个老生常谈的设计模式。之前我对于单例模式的理解仅仅停留在表面上,知道有几种,知道如何实现,知道大概的区别如何,但是其实单例模式还有很多不为人知的另一面,接下来我们就一起来看看。

单例模式的五种写法

方案一: 饿汉式

public class Singleton {   
    private static Singleton = new Singleton();
    private Singleton() {}
    public static getSignleton(){
        return singleton;
    }
}

方案二:懒汉式

public class Singleton {
    private static Singleton singleton = null;
    private Singleton(){}
    public static Singleton getSingleton() {
        if(singleton == null) singleton = new Singleton();
        return singleton;
    }
}

方案三:线程安全

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        synchronized (Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }    
}

方案四:双重校验锁

public class Singleton {
    private static volatile Singleton singleton = null;
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

方案五:静态内部类

public class Singleton {
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){}
 
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

方案六:枚举

public enum Singleton {
    INSTANCE;
}

单例模式的线程安全

方案一和方案二,非线程安全;
方案三和方案四,将唯一的实例进行判空操作,以及实例化的部分进行了synchronized进行加锁,实现了部分线程安全,那么还有什么隐患呢?那就是指令重排优化。
这里我们着重讲一下volatile修饰符,

volatile

volatile,有两层含义。第一层,可见性。第二层,防止重排序。
在类加载的过程中,分为以下几个步骤:

  • 分配内存空间
  • 初始化
  • 将 singleton 对象指向分配的内存地址

在没有volatile修饰之前,初始化和指向分配内存地址的执行顺序不可预测,那么就会导致一个问题:

在线程A正在进行类加载的过程中,恰好先执行了指向分配内存地址这一步,此时singleton已经不为null,但是由于还没有初始化,所以singleton也不能算是一个正常的实例对象。这个时候切换到线程B,由于singleton此时已经不是null,所以会直接返回,拿去调用一些方法,这个时候就会抛出异常。

volatile是如何保证线程安全的呢?

那么volatile的出现保证了在类加载的过程中,先进行初始化,再将singleton对象指向分配的内存地址。
在线程A进行类加载的时候,假如执行到初始化这一步,线程B的请求过来,由于此时singleton是null的,所以会被synchronized同步锁锁住,直到singleton实例化成功,再进行调用。

方案五,将实例放置在静态内部类中,静态内部类只会被加载一次,实现了线程安全。
方案六,枚举的内部实现自动帮我们实现了线程安全。

单例模式的反射安全

针对除了枚举单例以外的方案来说,为什么会有反射不安全呢?如果用户通过类的反射去调用构造函数,获取实例,那么我们的单例就不是真正意义上的单例模式了,那么如何解决这个问题呢?

在构造函数中,对singleton判空,如果非空,那我们就抛出异常,防止其反射调用。
【枚举自带防止反射强行调用的构造器】

单例模式的序列化安全

针对singleton实例的序列化和反序列化得到的对象是新的对象,那么这样就破坏了singleton的唯一性。
那么为什么在序列化的过程中会生成新的对象呢?
因为序列化会通过反射调用无参构造函数创建一个新的对象,如何避免呢?我们就回到了上一个问题,怎么去规避反射调用singleton的构造函数。
【枚举单例实现了自动序列化机制,防止了反序列化的时候创建新的对象】

总结

单例的四大要点:

  • 线程安全
  • 反射安全
  • 序列化与反序列化安全
  • 延迟加载

推荐阅读

为什么枚举单例的内部实现能保证线程安全以及序列化安全?

猜你喜欢

转载自blog.csdn.net/Android_Glimmer/article/details/89524445