java单例模式,双检为什么要加volatile?

303 阅读4分钟

前言

单例模式各种实现的讲解,为什么双检要加volatile?

单例模式

顾名思义就是只有一个对象实例,对外提供一个public方法获取这个实例

单例模式有七种实现方式:

  • 饿汉式(静态常量)
  • 饿汉式(静态代码块)
  • 懒汉式(线程不安全)
  • 懒汉式(线程安全,同步方法)
  • 双重检查
  • 静态内部类
  • 枚举

饿汉式(静态常量)

步骤:

  1. 私有化构造器,防止外部new
  2. 内部创建静态常量对象
  3. 对外提供public static方法获取实例
public class Singleton {
    private static final Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

优点:类加载时就创建对象,且只会有一次创建,没有线程安全问题。

缺点:类加载时就创建对象,类加载时机有很多种,并不一定会用到该对象,没有达到lazy loading效果,会造成内存浪费。

饿汉式(静态代码块)

public class Singleton {
    private static final Singleton instance;
    
    private Singleton() {}
    
    static {
        instance =  = new Singleton();
    }
    
    public static Singleton getInstance() {
        return instance;
    }
}

优缺点同上

懒汉式(线程不安全)

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

优点:起到了lazy loading效果

缺点:线程不安全

懒汉式单例模式的实现需要考虑线程安全性,可以通过 synchronized 关键字或者双重检查锁定等方式来实现。

懒汉式(线程安全,同步方法)

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

优点:起到了lazy loading,解决了线程安全问题

缺点:效率太低,每次访问都要等待锁,其实只需要第一次创建时加锁就可以了,后续直接返回即可

双重检查

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

这样就解决了懒加载问题,线程安全问题,也解决了效率问题。

总之,使用枚举方式实现单例模式是一种简单、高效双检为什么加volatile

双创检查需要volatile是为了禁止指令重排,new一个对象首先会分配内存空间,然后赋值初始化,在单线程情况下不会有问题,但是多线程情况下就可能因为编译器的指令重排导致我线程A在分配完内存空间后,线程B的指令重排导致在线程A还没完成对象初始化就开始执行外部的instance == null,这个情况下instance是不为null的,那就会获取到instance这个对象,但是这个对象只是分配了内存并没有完成初始化,属性都是为null的,就可能出现意想不到的问题,例如空指针。

加了volatile就保证了线程B的判断null的指令在我线程A完成instance初始化后才执行。

静态内部类

public class Singleton {
    private Singleton() {}
​
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
​
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

在这个例子中,我们定义了一个私有的静态内部类 SingletonHolder,这个内部类中定义了一个私有的静态变量 INSTANCE,它是 Singleton 的唯一实例。在 getInstance() 方法中,我们返回 SingletonHolder.INSTANCE,这样就可以获取到 Singleton 的唯一实例。

这种方式的优点是,由于静态内部类只有在被调用时才会被加载,因此可以实现懒加载的效果;同时,由于静态内部类只会被加载一次,因此可以保证线程安全。此外,使用静态内部类实现单例模式的代码比较简洁,也比较易于理解和维护。

枚举

使用枚举方式实现单例模式是一种非常简单、高效的方式,而且天然地保证了线程安全和防止反射攻击。

下面是一个使用枚举方式实现单例模式的示例代码:

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        // 单例对象的具体操作
    }
}

在这个例子中,我们定义了一个枚举类型 Singleton,并且在其中定义了一个 INSTANCE 实例,它是 Singleton 的唯一实例。由于枚举类型的特殊性质,它保证了实例只会被实例化一次,并且在多线程环境下也是安全的。我们可以通过 Singleton.INSTANCE 来获取单例对象。

另外,使用枚举方式实现单例模式还有一个很大的优点,就是可以防止反射攻击。由于枚举类型的构造函数默认是私有的,因此无法通过反射来创建枚举实例,从而保证了单例的唯一性。

总之,使用枚举方式实现单例模式是一种简单、高效、线程安全并且防止反射攻击的方式,可以考虑在实际开发中使用。