单例模式之线程安全的懒汉式

单例模式之线程安全的懒汉式

单例模式分为懒汉式和饿汉式,今天主要来讲讲其中的懒汉式,懒汉式因为要实现懒加载(使用时再创建对象),所以存在线程安全问题。
先来看看最简单的懒汉式:

public class Singleton {
    private static Singleton instance;

    private Singleton(){}

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

}

可以看到,当我们getInstance时,首先要经过if判断,在多线程场景下,就可能存在多个线程同时进入if判断,此时对象还未创建,那么就会多个线程都去创建对象,这样单例模式就被破坏了。

解决方案:

1. 双重检查

先上代码:

public class Singleton2 {
    private static volatile Singleton2 instance;
    
    private Singleton2(){}

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

用两次判断加同步代码块实现线程安全
用双重检查实现的懒汉式,在多线程场景中getIntance时,首先要进行一次对象是否已创建的判断,如果已创建就直接返回实例,当首次加载需要创建对象时,假设有线程A和线程B两个线程同时通过第一层判断,那么它们需要排队进入同步代码块,假设线程A先进入同步代码块,那么实例由线程A创建,那么当线程B进入同代码块时便不能通过第二层检查,即直接返回实例。这样便实现了线程安全的懒加载。

关于volatile关键字:
volatile有两个作用:保证可见性和防止指令重排
什么是保证可见性呢,就是当一个线程在对主内存的某一份数据进行更改时,改完之后会立刻刷新到主内存。并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。意思就是所有线程都可以得到最新的数据,这样一来就保证了可见性。
什么是指令重排呢,当new一个对象时,用字节码指令分析是三条指令(new、dup、invokespecial),这三条指令可能会发生重排序,引用指向分配地址,但对象还未创建,导致判空校验不准确。
因为创建对象的过程都在同步代码块中,所以此处使用volatile的作用主要是保证可见性。

2.静态内部类

代码:

public class Singleton3 {

    private Singleton3(){}

    private static class SingletonInstance{
        private static final Singleton3 INSTANCE = new Singleton3();
    }
    
    public static Singleton3 getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

静态内部类:1.当外部类被装载时,内部类并不会被装载 当使用到时才会被装载,且只装载一次。

3.枚举

代码:

enum Singleton4 {
    INSTANCE;
    public void method(){
        System.out.println("枚举实现单例");
    }
}

使用:
Singleton4.INSTANCE.method();
枚举是最简单也是最好用的实现方式,枚举的实际是用final修饰的实现enum接口的类,因为枚举构造只能私有,所以枚举是天生的单例模式
因为枚举类是在第一次访问时才被实例化,所以它也是懒加载的。

补充:

除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例。
如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容