java中的7种单例模式

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/y1962475006/article/details/79035250

java中的7种单例模式

单例模式是我们开发中经常会用到的,单例模式的好处是应用在运行时只有一个实例,生命周期从单例实例化后一直到应用生命周期结束。这里总结和比较一下几种单例的形式,共总结了七种。

写法一

public class Singleton1 implements Serializable {
    private static Singleton1 instance = new Singleton1();
    private Singleton1(){

    }
    public static Singleton1 getInstance(){
        return instance;
    }
}

这种写法的问题是,在class文件装载虚拟机的时候,就会分配内存。有时候我们不希望应用一启动就有个单例占了内存,尤其在android这种内存敏感的嵌入式设备中,希望可以在使用的时候再去分配,知道应用结束。但是这种写法是保证线程安全的,因为是在虚拟机加载class文件就分配的内存,虚拟机保证了单例的线程安全。

写法二

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

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

好,既然说Singleton1的写法不能实现延迟加载,那么就在获取单例的时候再初始化实例。但是这个写法有线程安全问题,详细见写法四。

写法三

public class Singleton3 {
    private static Singleton3 instance;
    private Singleton3(){

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

你有张良计我有过墙梯。说我线程不安全?加个锁给你看看。在静态方法前加锁,锁住了这个类(class)对象(而不是实例)。但是synchronized锁是个重量级锁,当一个线程访问这个类,如果这个对象已经被另一个线程访问时,这个线程会一直阻塞。每次调用getInstance()方法都要阻塞,这样是很耗性能的。

写法四

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

这是一个经典的DCL(双重检查锁定)写法。获取单例时,如果当前实例为空,就对class对象加锁,加锁后为防止多线程在加锁期间生成单例,又做了一次判定,如果这时候实例还是空,创建。每次使用getInstance方法,如果实例存在,就直接返回。最多阻塞一次。
这种写法目前看起来很完美,但是问题来了,

if(instance == null){
                    instance = new Singleton4();
                }

代码段和写法二中的一模一样。写法二有线程安全问题,代码四不是用synchroized处理了吗,还有线程安全的事?肯定有的,不然我不会费这力气说了。
在JMM(java内存模型)中,instance = new Singleton();可以分成几个指令:

  1. memory = allocate(memorySize);//分配一定大小的内存空间
  2. initMemory(memory);//初始化内存对象,比如初始化成员变量等等
  3. instance 指向 memory;//引用指向实例的地址

但是,了解JMM的童鞋应该知道,为了提高编译和运行效率,编译器和处理器会对代码做重排序(可以使指令对齐,处理器流水线更快),上述1-》2-》3的顺序可能会被重排成1-》3-》2,在单线程下,java语言规范保证了执行结果不变,但是在多线程下会出问题:

多线程示例

如图,A线程中,代码重排序成了1-》3-》2,在3执行之后,2之前,B线程访问instance,但是此时instance引用指向了分配的空间,但是这个空间并没有做实例对象的初始化工作!

这个问题的本质:

  1. 线程A中代码重排序了;
  2. 重排序就算了,在A没执行完全部代码之前,这个重排序让线程B看到了。

这个情况很少发生,但是真实发生的,这就悲催了。不过好在知道了什么原因,从根本入手解决。改进方案:

扫描二维码关注公众号,回复: 3309064 查看本文章
public class Singleton4 {
    private Singleton4(){}
    private volatile static Singleton4 instance;
    public static Singleton4 getInstance(){
        if(instance == null){
            synchronized (Singleton4.class){
                if(instance == null){
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}

就是在静态变量前加了volatile关键字!四苦一,一个volatile就解决了?原来是在JDK5以及以上,volatile的语义被增强了。在编译器编译时,有volatile声明的变量会在前后插入插入屏障,保证不会在单一线程看来都不会重排序。也就是通过阻止本质1实现线程安全。

写法五

上面说了阻止本质1实现线程安全,现在讲一个阻止本质2的写法:

public class Singleton5 {
    private Singleton5(){}
    private static class InstanceHolder{
        private static Singleton5 instance = new Singleton5();
    }
    public static Singleton5 getInstance(){
        return InstanceHolder.instance;
    }
}

就是套了一个私有的静态内部类。java语言规范保证了,一个类或者接口的静态成员在被赋值的时候,这个类会初始化(有个初始化锁,每个线程都会至少获取一次初始化锁保证初始化),这个过程比较复杂,结果就是对任意线程,内部可以重排序,但是这种重排序对其他线程不可见。这个也是Google大佬推荐的写法。

写法六

public enum  Singleton6 {
    INSTANCE;
    public void func(){

    }
}

黑科技式的写法。枚举类本质是继承了Enum类的静态类,但是enum会比普通静态变量更占内存,因为做了很多处理。JVM保证了static类的线程安全性。

写法七

public class Singleton7 {
    private static AtomicReference<Singleton7> instance = new AtomicReference<>();
    private Singleton7(){}
    public static Singleton7 getInstance(){
        for(;;){
            Singleton7 singleton7 = instance.get();
            if(singleton7!=null){
                return singleton7;
            }
            singleton7 = new Singleton7();
            if(instance.compareAndSet(null,singleton7)){
                return singleton7;
            }
        }
    }
}

高科技的写法。如果面试的时候让你写一种不用synchronized锁的线程安全的单例,这无疑是最佳答案(enum黑科技在JVM保证线程安全时一样用了synchronized)。这个写法用了CAS锁,CAS锁本质是用汇编cmpxchg指令实现锁cpu缓存。但是如果长时间获取不到锁,由于自旋(循环),会有较大的开销。

猜你喜欢

转载自blog.csdn.net/y1962475006/article/details/79035250