单例模式之双重检查锁(double check locking)的发展历程

不安全的单例

没有注意过多线程安全问题的时候,我们的单例可能是这样的:

public final class Singleton {

    private static Singleton instance;

    private Singleton () {
    }

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

这种写法在单线程中没有问题,但多线程中却会有两个引用对象,可以观察下两个线程调用的情况:

TimeThread AThread B
T1检查到instance为空
T2检查到instance为空
T3初始化对象A
T4返回对象A
T5初始化对象B
T6返回对象B

此使连个线程调用方分别拥有两个对象A、B的实例,就完全不是单例了

解决方法

加锁(synchronized)

public final class Singleton {

    private static Singleton instance;

    private Singleton () {
    }

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

在getInstance()方法中增加synchronized,保证了线程安全性,既简单又好理解,但性能不高,因为每次调用getInstance()方法都需要加锁(实际上只需要第一次初始化进行加锁),所以需要针对这个问题进行优化

双重检查锁

public final class Singleton {

    private static Singleton instance;

    private Singleton () {
    }

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

synchronized加锁仅发生在对象需要实例化的时候,否则其它情况都是直接返回已有的对象。

隐患(指令重排)

instance = new Singleton();这句代码可以分解成三步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

但有些编译器为了性能原因,可能会将第2步第3步进行重排序,重排后顺序可能就是:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

现在考虑重排后,两个线程发生了以下调用:

TimeThread AThread B
T1检查到instance为空
T2获取锁
T3再次检查到instance为空
T4为instance分配内存空间
T5将instance指向内存空间(重排后的第2步)
T6检查到instance不为空
T7访问instance(此时对象还未完成初始化)
T8初始化instance(重排后的第3步)

这种情况下Thread B访问的是一个还没有初始化的对象。

解决指令重排隐患(最正确的双重锁方式)
public final class Singleton {

    private volatile static Singleton instance;

    private Singleton () {
    }

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

在instance字段增加volatile后,防止了指令重排,按照预想的执行顺序先初始化对象再指向内存空间,并且所有的写(write)操作都将发⽣在读(read)操作之前

扩展

在对象实例化时的加锁性能可以再进行优化,那就是通过加入局部变量的方式,性能可以提高25%,可以参考《Effective Java, Second Edition》 p. 283-284

    public static Singleton getInstance() {
        // 局部变量将性能提高25%,
        Singleton result = instance;
        // 单例双重检查
        // 第一重
        if (result == null) {
            synchronized (Singleton.class) {
                result = instance;
                // 第二重
                if (result == null) {
                    instance = result = new Singleton();
                }
            }
        }
        return result;
    }

总结

通过多个示例可以看出,无论加双重锁、还是加volatile,都是事出有因,只要我们了解了背后的根本原因,就很容易理解为什么要这么写了

如果你喜欢我的文章,记得一键三连(不要下次一定)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我思知我在

原创不易,多多一键三连

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值