走自己的路

路漫漫其修远兮,吾将上下而求索

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  50 随笔 :: 4 文章 :: 118 评论 :: 0 Trackbacks
 

在这本书中文版的第219页有个例子,讲lazy load时用到double checkdouble check比直接用同步的好处是,当Singleton初始化后,就不会有额外的同步操作。它的例子是

 1public class Singleton {
 2    private volatile static Singleton INSTANCE;
 3    
 4    private Singleton() {
 5        
 6    }

 7    
 8    public static Singleton getInstance() {
 9        if(INSTANCE == null{
10            synchronized (Singleton.class{
11                if(INSTANCE == null{
12                    INSTANCE = new Singleton();
13                }

14            }

15        }

16        return INSTANCE;
17    }

18}

19
 

        不幸的是,双重检查不会保证正常工作,因为编译器会在Singleton的构造方法被调用之前随意给INSTANCE先付一个值。如果在INSTANCE引用被赋值之后而被初始化之前线程1被切换,线程2就会被返回一个对未初始化完全的单例类实例的引用。这样在程序的其他方法中使用时可能会出现未知的错误。

 

个人一开始认为正确的写法,应该是这样的

 1public class SingletonNew {
 2    private volatile static SingletonNew INSTANCE;
 3    
 4    private SingletonNew() {
 5        
 6    }

 7    
 8    public static SingletonNew getInstance() {
 9        SingletonNew tempInstance = INSTANCE;
10        if(tempInstance == null{
11            synchronized (Singleton.class{
12                tempInstance = INSTANCE;    //(1)
13                if(tempInstance == null{
14                    INSTANCE = tempInstance = new SingletonNew(); //(2)
15                }

16            }

17        }

18        return tempInstance;
19    }

20}

21
 

      

     利用一个tempInstance局部变量来排除返回实例未初始化完全的情况。因为每次判断的都是局部变量,每个线程都会有一个自己的tempInstance,这样就保证每个线程的tempInstance要么是初始化完全的要么就是未初始化的,不会出现中间的情况。要注意的是SingletonNew(1)处是不能去掉的,比如线程构造了一个实例,线程2此时等待在那里,线程2得到锁,判断tempInstance == null结果是true,又初始化了一次,这就不是单例了。(2)处的赋值顺序也是不能颠倒的,如果颠倒就会出现和Singleton类一样的情形。


请大家详细讨论,详细解释一下。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    其实这两种写法在旧的JMM上都是错误的,在新的JMM上都是对的,错误的原因主要是JMM对代码的重新排序和优化,新的JMM又对volatile的语义进行了扩展,保证了double-check的正确性。很抱歉一开始让一些博友产生了困惑,谢谢大家的热心的讨论和回帖,我的主要问题就是出现在对JMM了解不够深入,只是碎片式的了解一些,没有很好的了解编译器对代码的重新排序和优化,当然编译原理课上是学过的。二又没有很好的掌握到volatile的新的语义。其实对一些细节了解清楚,可以避免我们的代码出现一些奇怪的问题,特别是在多线程环境中。

 

    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的,编译器可能就会对writerreader中的代码进行重新排序,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



posted on 2008-07-23 19:51 叱咤红人 阅读(2538) 评论(22)  编辑  收藏 所属分类: Design and Analysis Pattern J2SE and JVM

评论

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-23 21:20 路过
即使第一种写法有问题,你怎么能证明第二种写法就是对的呢?
照你的说法INSTANCE是一个没有完全初始化的对象,那么tempInstance是复制的引用而已,前者没有完全初始化后者也肯定是一样的。我完全没看出来多赋值一次有什么好处。
请设计一个试验,谢谢!
  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-23 23:10 Jarod
博主是在乱说

private volatile static SingletonNew INSTANCE;
static {
System.out.println(INSTANCE); //null
}

就算“因为编译器会在Singleton的构造方法被调用之前随意给INSTANCE先付一个值”成立了,代码2不见得就解决了问题  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误[未登录] 2008-07-24 06:29 叱咤红人
@Jarod
谢谢回复.
INSTANCE = new Singleton();我的理解是调用了构造函数,在构造之前会先生成一个临时的值,引用指向一个临时的地方,具体以前在那里看到的也不太记得了.所以第一种方法线程1进入构造函数后,线程2会得到一个不是null的临时值,所以会得到一个未初始化完全的对象.第二种方法,对全局静态变量INSTANCE,没有用它来作为double check的条件,而是使用了tempInstance局部变量,每个线程都会生成一个自己的tempInstance   回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 07:48 朱远翔-Apusic技术顾问
@叱咤红人
在进入初始化之前使用的是线程同步,那么就不存在线程切换的问题呀?   回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 08:13 ldd600
@朱远翔-Apusic技术顾问
谢谢回复。
因为采用了double check,延迟了同步。所以还是存在线程切换的问题。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 08:46 5452
double check这个东西,现在说不清楚,这种方法没有办法确定就是单例。
JVM建立对象的过程是这样的:1、先分配一块内存,2、然后把内存地址赋值给对象的引用,3、然后调用类的构造函数,生成对象。
如果一个线程执行到第二步的时候,另外一个线程进入这个方法,这个时候INSTANCE已经不是空的了,但是实际上还没有初始化,这样的话,一定会出问题的~
  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 09:24 路过
楼主所提出的问题我可以理解,可是无法理解
“ if(INSTANCE == null) {”

“ SingletonNew tempInstance = INSTANCE;
if(tempInstance == null) {”
这两句会得到不同的判断。
如果INSTANCE没有完全初始话,tempInstance也肯定是一样啊。虽然“每个线程都会生成一个自己的tempInstance”,其实这些tempInstance和INSTANCE没有区别,它们是不同的引用,但是指向同一个对象。
  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 09:29 ldd600
@路过
谢谢回复
每个线程生成自己的tempInstance是指这句
INSTANCE = tempInstance = new SingletonNew(); //(2)
这句保证了INSTANCE的构造的完全性。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 09:38 yswift
JAVA不支持double check,不管怎么修改,只要用到double check都是错的,在C++中,书中的例子是完全可以正常工作的。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 10:07 路人
好像都没说道正点上,注意volatile关键字。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 10:29 白色天堂
这段代码在jdk1.5之后完全没有问题。之前的版本可能出问题。

你也没有理解出错的原因,所作的改动完全是画蛇添足。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 11:06 dennis
无语了,没看到volatile关键字吗?  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中的一个错误 2008-07-24 11:07 dennis
@白色天堂
也不能说完全没问题,有的jvm实现在volatile的语义上还是有问题的,只能说在sun jdk1.5及以后版本是没有问题的。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-24 12:38 路过
麻烦楼上的讲一下为什么
INSTANCE = tempInstance = new SingletonNew(); //(2)
这句保证了INSTANCE的构造的完全性。
谢谢。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-24 12:42 路过
volatile在这段程序里起了什么作用呢?
楼主说的是得到了一个引用但是引用指向的对象是没有完全初始化的,又不是说对象已经初始化了还是有程序得到了null的引用。
麻烦楼上的解释一下,谢谢。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-24 13:09 zhuxing
@yswift

yswift同志说的一针见血!  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-25 09:32 dennis
http://www.ibm.com/developerworks/java/library/j-dcl.html?loc=j

看看这篇文章,俺就不多说了。原因就在于JMM模型的out-of-order writes问题。jdk5通过正确的实现volatile语义能保证对声明为volatile的变量的读和写不会被后续的读和写所重排序,因而解决了这个问题。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-25 11:09 路过
The best solution to this problem is to accept synchronization or use a static field.
多谢dennis,学习了。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-26 21:44 叱咤红人
谢谢大家尤其是dennis的热情讨论和回复,其实这两种写法在旧的JMM上都是错误的,在新的JMM上都是对的,我主要还是没有对JMM有更深入的理解,抱歉,继续努力好好工作,好好学习,大家分享。  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-26 21:46 叱咤红人
其实这两种写法在旧的JMM上都是错误的,在新的JMM上都是对的,错误的原因主要是JMM对代码的重新排序和优化,新的JMM又对volatile的语义进行了扩展,保证了double-check的正确性。很抱歉一开始让一些博友产生了困惑,谢谢大家的热心的讨论和回帖,我的主要问题就是出现在对JMM了解不够深入,只是碎片式的了解一些,没有很好的了解编译器对代码的重新排序和优化,当然编译原理课上是学过的。二又没有很好的掌握到volatile的新的语义。其实对一些细节了解清楚,可以避免我们的代码出现一些奇怪的问题,特别是在多线程环境中。


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关系。
  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗? 2008-07-26 21:53 zxbyh
不用去研究这个!
使用饿汉单例模式就可以了.

<<java 与模式>>  回复  更多评论
  

# re: 《Head First Design Pattern 单例模式》中double check有问题吗?[未登录] 2008-08-08 13:26 Chris
不管哪种方法,在多机的情况下依然还是解决不了单例的问题,现在机器那么廉价,那点延迟初始化所带来的效率是微乎其微的,完全不需要。  回复  更多评论
  


只有注册用户登录后才能发表评论。


网站导航: