返回信息流下面这段程序是网上讲单例模式的,里面用到了volatile,说这个关键字可以禁止指令重排序优化。
public class Singleton
{
private volatile static Singleton singleton = null;
private Singleton() { }
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
那为什么下面SingletonHolder里的INSTANCE就不用volatile了呢?
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这是一条镜像帖。来源:北邮人论坛 / java / #41136同步于 2015/5/26
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Java机器人发帖
[问题]关于Java volatile修饰符
glifeng0
2015/5/26镜像同步7 回复
订阅后,新回复会通过你的通知中心匿名送达。
7 条回复
好像有很多blog讲这个问题的,第一段程序是一个双重检查加锁。因为在new的过程中可能有指令重排导致其他线程使用未初始化的对象。
http://ifeve.com/double-checked-locking-with-delay-initialization/
搜了下发现这个讲的不错。
第二个不用volatile,因为INSTANCE已经是final了,不可变了,所以不管多少个线程看到的值应该都是一样的。
【 在 glifeng0 的大作中提到: 】
: 下面这段程序是网上讲单例模式的,里面用到了volatile,说这个关键字可以禁止指令重排序优化。
: [code=java]
: public class Singleton
: ...................
前者有一个陷阱:因为多线程,以及CPU之间Cache一致性的问题,有可能另一个线程看到singleton确实指向新创建的对象,但却还没有看到Singleton()的构造函数对其成员进行的赋值。volatile做了一个保证:任何线程只要看到singleton指向新的对象,那么一定会看到对singleton赋值(那个singleton = ...表达式)之前对内存的所有操作,这自然包括Singleton()构造函数里做的所有的事。
后者就没这种问题了:只要看到SingletonHolder这个类,然后去读他的final field INSTANCE,然后读INSTANCE引用的对象内部的东西,那么一定看到INSTANCE对应的对象的构造函数写入的东西。(尼玛这语义太微妙了,涉及到地址依赖,简直堪比C++11的release-consume)总之,这样写没问题的,不需要volatile。
原来初始化的时候也有个锁,谢谢啦!
【 在 aiquestion 的大作中提到: 】
: 好像有很多blog讲这个问题的,第一段程序是一个双重检查加锁。因为在new的过程中可能有指令重排导致其他线程使用未初始化的对象。
: http://ifeve.com/double-checked-locking-with-delay-initialization/
: 搜了下发现这个讲的不错。
: ...................
前面一段明白了,后面一段看不懂。
【 在 nuanyangyang 的大作中提到: 】
: 前者有一个陷阱:因为多线程,以及CPU之间Cache一致性的问题,有可能另一个线程看到singleton确实指向新创建的对象,但却还没有看到Singleton()的构造函数对其成员进行的赋值。volatile做了一个保证:任何线程只要看到singleton指向新的对象,那么一定会看到对singleton赋值(那个singleton = ...表达式)之前对内存的所有操作,这自然包括Singleton()构造函数里做的所有的事。
: 后者就没这种问题了:只要看到SingletonHolder这个类,然后去读他的final field INSTANCE,然后读INSTANCE引用的对象内部的东西,那么一定看到INSTANCE对应的对象的构造函数写入的东西。(尼玛这语义太微妙了,涉及到地址依赖,简直堪比C++11的release-consume)总之,这样写没问题的,不需要volatile。
【 在 glifeng0 的大作中提到: 】
: 前面一段明白了,后面一段看不懂。
这说来话长了。
为了提高性能,CPU可能会重新调整读、写操作的顺序。这叫“out-of-order execution”。
x86CPU不用讨论了,因为它的顺序太强了:它拒绝把任何读写操作往前移到另一个读操作之前,也拒绝把任何读写操作移到另一个写操作之后。可以把所有的读想象成“加锁”,把所有的写当成“解锁”,CPU拒绝把加锁和解锁之间的操作移到外面去。
另外一些CPU太弱了:任何两个读写都可以随便交换,要强制它不能换,必须加fence。
还有一些CPU(ARM, POWER等)比较特殊:如果一个指令依赖另一个指令的结果(数据依赖),那么CPU就不会交换这两个指令的顺序。常见的例子包括:第一条指令从内存读了一个指针,第二条指令从读到的这个指针和一个偏移量读取数据。CPU不会交换这两条指令的顺序。这种规则的特殊之处在于:这两条指令必须有数据依赖关系。也就是说,如果没有依赖关系,那么CPU就可以交换它们的顺序。
Java的关于final的规则是这样说的:当构造函数执行完之后,会对这个对象里的所有final field做“freeze操作”。假设有一个写操作w,然后发生了一个freeze操作f,然后发生了一个操作a,a不是对final field的操作(可以是把这个构造好的对象赋值给一个变量),如果有一个读操作r1,这个r1依赖于a(比如是从a赋值的变量里获得的引用读的),而且r1读了final field,还有另一个读操作r2读了r1指向的对象里的域(显然r2依赖r1)。如果上述“然后”理解成happen-before关系,那么Java可以保证r2发生于w之后。
关键就是这里面的两个“依赖”关系。这对应了CPU里的这种数据依赖,在这种尊重数据依赖的CPU上很容易实现。如果不是的话,就要加fence了。
对应到这个例子,Java的静态成员可以看成专门有一个对象装着。Java保证它的构造在调用它的方法之前完成。然后因为那个INSTANCE是final field,所以,如果先读到INSTANCE的引用,再去读里面的域,就有了依赖关系。这样就一定能读到它的构造函数里写入的数据。
好复杂。。。,谢谢啦!
【 在 nuanyangyang 的大作中提到: 】
:
: 这说来话长了。
: 为了提高性能,CPU可能会重新调整读、写操作的顺序。这叫“out-of-order execution”。
: ...................
想知道,怎么能到你这个水平?知识面好宽
【 在 nuanyangyang 的大作中提到: 】
:
: 这说来话长了。
: 为了提高性能,CPU可能会重新调整读、写操作的顺序。这叫“out-of-order execution”。
: ...................