并发编程实战(3). 安全发布对象 之 单例模式的7种实现

摘自Java Concurrency In Practice

安全发布对象的常用模式

  • 1.在静态初始化函数中初始化一个对象引用
  • 2.将对象的引用保存到volatile类型的域或者AtomicReference对象中
  • 3.将对象的引用保存到某个正确构造对象的final域中
  • 4.将对象的引用保存到一个由锁保护的域中

这里介绍一下懒汉模式,它代码实现的演进包含了除了3之外的所有模式。

文章目录

    • 懒汉模式最简版
    • 饿汉模式
    • 懒汉模式+Syn函数
    • 懒汉模式+DoubleCheck
    • 懒汉模式+DoubleCheck+volatile
    • 再看 饿汉模式
    • 枚举单例,最安全

懒汉模式最简版

先看一下单例模式的最简单实现,懒加载,(懒汉模式)。
线程交叉执行会导致它不安全:

@NotThreadSafe
public class LazySingleton {

    // 私有构造
    private LazySingleton(){ }

    private static LazySingleton lazySingleton;

    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

饿汉模式

再看看饿汉模式。如果实例用得次数较少,这种实现可能会导致资源浪费。

@ThreadSafe
public class StarvingSingleton {

    private StarvingSingleton() { }

    private static StarvingSingleton instance = new StarvingSingleton();

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

懒汉模式+Syn函数

synchronized同步函数,会导致多线程并发效率底下。

@ThreadSafe //虽然是线程安全的
@NotRecommend  // 但是仍然不推荐
public class LazySynSingleton {

    // 私有构造
    private LazySynSingleton(){ }

    private static LazySynSingleton instance;
    
    // synchronized修饰函数,并发量不大
    public static synchronized LazySynSingleton getInstance(){
        if(instance == null){
            instance = new LazySynSingleton();
        }
        return instance;
    }
}

懒汉模式+DoubleCheck

@NotThreadSafe
public class LazySyn2Singleton {

    // 私有构造
    private LazySyn2Singleton(){ }

    private static LazySyn2Singleton instance;

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

多线程下指令重排序会导致其出问题。
实例化一个对象正常情况下分3步:

  1. memory = allocate() 开辟一段内存空间
  2. initInstance() 初始化内存空间(初始化对象)
  3. instance = memory 实例变量指向内存空间

所以说正常情况会按照1-2-3的顺序来实例化对象。然而,受指令重排序的影响,这3步 很可能会变成1-3-2:

1.memory = allocate() 开辟一段内存空间

3.instance = memory 实例变量指向内存空间

2.initInstance() 初始化内存空间(初始化对象)

那么很可能会出现下面的情况

if(instance == null){
    // 线程B运行到这里
    synchronized (LazySyn2Singleton.class){
        if(instance == null){
            // 线程A运行到这里,但是只运行了第1,3步后
            // CPU调度,使线程B可以继续运行
            instance = new LazySyn2Singleton();
        }
    }
}
return instance;
// 那么线程B可以运行到return这里
// 而实际上,线程A并没有将对象初始化
// 所以得到的对象并非一个构造完全的对象

懒汉模式+DoubleCheck+volatile

上面的懒汉模式是非线程安全的,因为可能存在指令重排序造成的“实例构造不完全就被溢出”的危险。
那么我们何不解决这个指令重排序的问题,不让它指令重排序呢?

这就让我们想到了volatile关键字,它能禁止指令重排序!

@NotThreadSafe
public class LazySyn2Singleton {

    // 私有构造
    private LazySyn2Singleton(){ }
    
    // 使用volatile关键字
    private volatile  static LazySyn2Singleton instance;

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

再看 饿汉模式

我们这里使用静态代码块,能够保证安全性。

@ThreadSafe
@Slf4j
public class Starving2Singleton {

    private Starving2Singleton() { }

    static {
        instance = new Starving2Singleton();
    }

    private static Starving2Singleton instance;
    
    public static Starving2Singleton getInstance(){
        return instance;
    }

    public static void main(String[] args) {

        log.info("Instance1, {}", getInstance());
        log.info("Instance2, {}", getInstance());
    }
}

如果上面的语句改为

private static Starving2Singleton instance = null;

那么,我们就需要注意static代码块与行内初始化语句的先后顺序了。

枚举单例,最安全

相比饿汉模式,它在实际调用时才会初始化,类似懒汉。
相比懒汉模式,它安全性有保证,且代码比较简单。

@ThreadSafe
@Recommend
public class EnumSingleton {

    private EnumSingleton() {
    }

    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton{
        INSTANCE;

        private EnumSingleton instance;

        // JVM保证这个方法绝对只调用一次
        Singleton(){
            instance = new EnumSingleton();
        }

        public EnumSingleton getInstance(){
            return instance;
        }
    }
}

你可能感兴趣的:(并发编程)