Skip to main content

线程安全的单例模式

huhxAbout 3 minjavaThreadConcurrency

单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。单例与多线程没啥直接关系,但是保证多线程下单例模式的安全性,是件有趣且好玩的事情。今天我们就来聊聊这单例。

单例模式

饿汉模式

public class Singleton {
    private final static Singleton INSTANCE = new Singleton();

    private Singleton() {}
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

这种单例模式是线程安全的,没有加锁。但是对于变量INSTANCE会在类的首次初始化时被创建。

懒汉模式

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() throws InterruptedException {
        if (INSTANCE == null) {
            TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}






 






这种会有问题的,当多个线程同时执行到第7行,都判断INSTANCE没有被初始化,然后多个线程都进行了初始化,从而生成了多个实例。

延迟加载解决方案之声明synchronized

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {}

    public synchronized static Singleton getInstance() throws InterruptedException {
        if (INSTANCE == null) {
            TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

这种是线程安全的,因为getInstance方法加了synchronized,所以保证了这个方法只能同时被一个线程访问。

延迟加载解决方案之同步代码块

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() throws InterruptedException {
        synchronized (Singleton.class) {
            if (INSTANCE == null) {
                TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }
}

这种是线程安全的,和上面的写法很类似。synchronized加在了要保护的代码块上,保证了判断和初始化逻辑是线程安全了。

延迟加载解决方案之同步部分代码块

public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() throws InterruptedException {
        if (INSTANCE == null) {
            TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
            synchronized (Singleton.class) {
                INSTANCE = new Singleton();
            }
        }
        return INSTANCE;
    }
}

这种是线程不安全的

延迟加载解决方案之DCL双检查锁机制

public class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() throws InterruptedException {
        if (INSTANCE == null) {
            TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

 








 






这种是线程安全的,INSTANCE采用volatile关键字修饰也是很有必要的。

INSTANCE = new Singleton();

这段代码其实是分为三步执行

  1. INSTANCE分配内存空间
  2. 初始化INSTANCE
  3. INSTANCE指向分配的内存地址

但是由于JVM具有指令重排的特性,执行顺序有可能变成1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程T1执行了1和3,此时T2 调用getInstance()后发现INSTANCE不为空,因此返回INSTANCE,但此时INSTANCE还未被初始化。

总结

FAQ

参考