路漫漫其修远兮,吾将上下而求索
在这本书中文版的第219页有个例子,讲lazy load时用到double check,double check比直接用同步的好处是,当Singleton初始化后,就不会有额外的同步操作。它的例子是
不幸的是,双重检查不会保证正常工作,因为编译器会在Singleton的构造方法被调用之前随意给INSTANCE先付一个值。如果在INSTANCE引用被赋值之后而被初始化之前线程1被切换,线程2就会被返回一个对未初始化完全的单例类实例的引用。这样在程序的其他方法中使用时可能会出现未知的错误。
个人一开始认为正确的写法,应该是这样的
利用一个tempInstance局部变量来排除返回实例未初始化完全的情况。因为每次判断的都是局部变量,每个线程都会有一个自己的tempInstance,这样就保证每个线程的tempInstance要么是初始化完全的要么就是未初始化的,不会出现中间的情况。要注意的是SingletonNew的(1)处是不能去掉的,比如线程构造了一个实例,线程2此时等待在那里,线程2得到锁,判断tempInstance == null结果是true,又初始化了一次,这就不是单例了。(2)处的赋值顺序也是不能颠倒的,如果颠倒就会出现和Singleton类一样的情形。
Jvm编译器会对生成的代码进行优化,重新排序,甚至移除它认为不必要的代码,volatile变量之间也是没有顺序保证的。然而jvm保证了classloader load字节码和静态变量初始化的同步性,所有把singleton设置为静态变量是没有问题的。JMM保证了单线程执行的效果和程序的顺序是相同的。JVM对代码的重新排序和优化是对于程序不可见的,所以在例子2中我不应该假设执行的顺序。在读volatile变量之前,写行为确保执行完毕,并且更新的值会从线程工作内存(CPU缓存,寄存器)刷新到主内存中,JMM禁止volatile读入寄存器,其他线程读取时也会重新load到工作内存中,保证了一致性和可见性,避免读取脏数据。以前一直以为volatile涉及的只是变量可见性问题,或者说对可见性的适用范围没有很好的理解,并不涉及JMM顺序性和原子性问题。新的JMM对它进行了扩展,它对volatile变量的重新排序也做了限制。在旧的内存模型当中,volatile变量的多次访问之间是不能重新排序的,但是它们能在和对非volatile变量访问代码之间进行重新排序,新的内存模型不同的是,volatile访问行为在和非volatile变量的访问行为的代码之间重新排序加了一些限制。对volatile的写行为就和synchronize方法或block释放监视器(锁)的效果是一样的,对volatile字段的读操作和监视器(锁)的申请效果也是一样的。新的模型在volatile字段访问上做了一些严格的限制,只对当前线程可见的变量写入到volatile共享变量f后,当其他线程读取f后就是可见的。
下面这个简单的例子:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
假设当前一个线程正在调用writer方法,其他线程正在调用reader方法,writer方法中对v的写行为将对x的写行为释放到了内存中,v变量的读取,又重新从内存中获取了新值。因此,如果读方法看到了v的值被设为true,也保证了它在这之前就可以看到x的新值42,但这在旧的内存模型中是不保证的。如果v不是volatile的,编译器可能就会对writer和reader中的代码进行重新排序,reader方法的访问有可能得到的x就是0. 可见在新的JMM中,volatile的语义得到了很好的加强,每次对volatile字段的读和写可看作是都是半同步。这种顺序性(happen-before关系)是针对同一个volatile字段而言的,对不同volatile字段的读取还是没有这种顺序保证的。在新的JMM下,用volatile就可以解决问题,线程1实例的初始化和线程2的读取volatile变量就存在一个happen-before关系。 JMM对顺序性只是提出了一些规则,具体如何重新排序还是不得而知。 参考文章:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#reordering 《JAVA Language Specification》 17.4
Powered by: BlogJava Copyright © 叱咤红人