不安全的单例
没有注意过多线程安全问题的时候,我们的单例可能是这样的:
public final class Singleton {
private static Singleton instance;
private Singleton () {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton ();
}
return instance;
}
}
这种写法在单线程中没有问题,但多线程中却会有两个引用对象,可以观察下两个线程调用的情况:
Time | Thread A | Thread 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();
这句代码可以分解成三步骤:
- 分配内存空间
初始化对象
将对象指向刚分配的内存空间
但有些编译器为了性能原因,可能会将第2步
和第3步
进行重排序,重排后顺序可能就是:
- 分配内存空间
将对象指向刚分配的内存空间
初始化对象
现在考虑重排后,两个线程发生了以下调用:
Time | Thread A | Thread 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,都是事出有因,只要我们了解了背后的根本原因,就很容易理解为什么要这么写了
如果你喜欢我的文章,记得一键三连(不要下次一定)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/17889.html