DANCE WITH JAVA

开发出高质量的系统

常用链接

统计

积分与排名

好友之家

最新评论

Java中的模式 --单态 (部分翻译 double-checked locking break)

单态定义:
Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。

Singleton模式就为我们提供了这样实现的可能。使用Singleton的好处还在于可以节省内存,因为它限制了
实例的个数,有利于Java垃圾回收(garbage collection)。

使用Singleton注意事项:
有时在某些情况下,使用Singleton并不能达到Singleton的目的,如有多个Singleton对象同时被不同的类
装入器装载;在EJB这样的分布式系统中使用也要注意这种情况,因为EJB是跨服务器,跨JVM的

单态模式的演化:
单态模式是个简单的模式,但是这个简单的模式也有很多复杂的东西。

(注意:在这里补充一下,现在单态模式其实有一个写法是不错的见这里:http://www.blogjava.net/dreamstone/archive/2007/02/27/101000.html,但还是建议看完这篇文章,因为解释的事情是不一样的,这里说的是为什么double-checked不能使用.)
一,首先最简单的单态模式,单态模式1
import java.util.*;
class Singleton
{
  private static Singleton instance;
  private Vector v;
  private boolean inUse;

  private Singleton()
  {
    v = new Vector();
    v.addElement(new Object());
    inUse = true;
  }

  public static Singleton getInstance()
  {
    if (instance == null)          //1
      instance = new Singleton();  //2
    return instance;               //3
  }
}
这个单态模式是不安全的,为什么说呢 ?因为没考虑多线程,如下情况
Thread 1 调用getInstance() 方法,并且判断instance是null,然後进入if模块,
在实例化instance之前,
Thread 2抢占了Thread 1的cpu
Thread 2 调用getInstance() 方法,并且判断instance是null,然後进入if模块,
Thread 2 实例化instance 完成,返回
Thread 1 再次实例化instance
这个单态已经不在是单态

二,为了解决刚才的问题:单态模式2
public static synchronized Singleton getInstance()
{
  if (instance == null)          //1
    instance = new Singleton();  //2
  return instance;               //3
}
采用同步来解决,这种方式解决了问题,但是仔细分析
正常的情况下只有第一次时候,进入对象的实例化,须要同步,
其它时候都是直接返回已经实例化好的instance不须要同步,
大家都知到在一个多线程的程序中,如果同步的消耗是很大的,很容易造成瓶颈

三,为了解决上边的问题:单态模式3,加入同步
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {
      instance = new Singleton();
    }
  }
  return instance;
}
同步改成块同步,而不使用函数同步,但是仔细分析,
又回到了模式一的状态,再多线程的时候根本没有解决问题

四,为了对应上边的问题:单态模式4,也就是很多人采用的Double-checked locking
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {  //1
      if (instance == null)          //2
        instance = new Singleton();  //3
    }
  }
  return instance;
}
这样,模式一中提到的问题解决了。不会出现多次实例化的现象
当第一次进入的时候,保正实例化时候的单态,在实例化后,多线程访问的时候直接返回,不须要进入同步模块,
既实现了单态,又没有损失性能。表面上看我们的问题解决了,但是再仔细分析:
我们来假象这中情况:
Thread 1 :进入到//3位置,执行new Singleton(),但是在构造函数刚刚开始的时候被Thread2抢占cpu
Thread 2 :进入getInstance(),判断instance不等于null,返回instance,
(instance已经被new,已经分配了内存空间,但是没有初始化数据)
Thread 2 :利用返回的instance做某些操做,失败或者异常
Thread 1 :取得cpu初始化完成
过程中可能有多个线程取到了没有完成的实例,并用这个实例作出某些操做。
-----------------------------------------
出现以上的问题是因为
mem = allocate();             //分配内存
instance = mem;               //标记instance非空
                              //未执行构造函数,thread 2从这里进入
ctorSingleton(instance);      //执行构造函数
                              //返回instance
------------------------------------------                             

五,证明上边的假想是可能发生的,字节码是用来分析问题的最好的工具,可以利用它来分析
下边一段程序:(为了分析方便,所以渐少了内容)
字节码的使用方法见这里,利用字节码分析问题
class Singleton
{
  private static Singleton instance;
  private boolean inUse;
  private int val; 

  private Singleton()
  {
    inUse = true;
    val = 5;
  }
  public static Singleton getInstance()
  {
    if (instance == null)
      instance = new Singleton();
    return instance;
  }
}
得到的字节码                           
;asm code generated for getInstance
054D20B0   mov         eax,[049388C8]      ;load instance ref
054D20B5   test        eax,eax             ;test for null
054D20B7   jne         054D20D7
054D20B9   mov         eax,14C0988h
054D20BE   call        503EF8F0            ;allocate memory
054D20C3   mov         [049388C8],eax      ;store pointer in
                                           ;instance ref. instance 
                                           ;non-null and ctor
                                           ;has not run
054D20C8   mov         ecx,dword ptr [eax]
054D20CA   mov         dword ptr [ecx],1   ;inline ctor - inUse=true;
054D20D0   mov         dword ptr [ecx+4],5 ;inline ctor - val=5;
054D20D7   mov         ebx,dword ptr ds:[49388C8h]
054D20DD   jmp         054D20B0

上边的字节码证明,猜想是有可能实现的

六:好了,上边证明Double-checked locking可能出现取出错误数据的情况,那么我们还是可以解决的
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {      //1
      Singleton inst = instance;         //2
      if (inst == null)
      {
        synchronized(Singleton.class) {  //3
          inst = new Singleton();        //4
        }
        instance = inst;                 //5
      }
    }
  }
  return instance;
}
利用Double-checked locking 两次同步,中间变量,解决上边的问题。
(下边这段话我只能简单的理解,翻译过来不好,所以保留原文,list 7是上边的代码,list 8是下边的
The code in Listing 7 doesn't work because of the current definition of the memory model.
 The Java Language Specification (JLS) demands that code within a synchronized block
 not be moved out of a synchronized block. However, it does not say that
 code not in a synchronized block cannot be moved into a synchronized block.
A JIT compiler would see an optimization opportunity here.
This optimization would remove the code at
//4 and the code at //5, combine it and generate the code shown in Listing 8:)
------------------------------------------------- 
list 8
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {      //1
      Singleton inst = instance;         //2
      if (inst == null)
      {
        synchronized(Singleton.class) {  //3
          //inst = new Singleton();      //4
          instance = new Singleton();              
        }
        //instance = inst;               //5
      }
    }
  }
  return instance;
}
If this optimization takes place, you have the same out-of-order write problem we discussed earlier.
如果这个优化发生,将再次发生上边提到的问题,取得没有实例化完成的数据。
-------------------------------------------------

以下部分为了避免我翻译错误误导打家,保留原文

Another idea is to use the keyword volatile for the variables inst and instance.
According to the JLS (see Resources), variables declared volatile are supposed to
be sequentially consistent, and therefore, not reordered.
But two problems occur with trying to use volatile to fix the problem with
double-checked locking:

The problem here is not with sequential consistency.
Code is being moved, not reordered.

Many JVMs do not implement volatile correctly regarding sequential consistency anyway.
The second point is worth expanding upon. Consider the code in Listing 9:

Listing 9. Sequential consistency with volatile

class test
{
  private volatile boolean stop = false;
  private volatile int num = 0;

  public void foo()
  {
    num = 100;    //This can happen second
    stop = true;  //This can happen first
    //...
  }

  public void bar()
  {
    if (stop)
      num += num;  //num can == 0!
  }
  //...
}
 
According to the JLS, because stop and num are declared volatile,
they should be sequentially consistent. This means that if stop is ever true,
num must have been set to 100.
However, because many JVMs do not implement the sequential consistency feature of volatile,
you cannot count on this behavior.
Therefore, if thread 1 called foo and thread 2 called bar concurrently,
thread 1 might set stop to true before num is set to 100.
This could lead thread 2 to see stop as true, but num still set to 0.
There are additional problems with volatile and the atomicity of 64-bit variables,
but this is beyond the scope of this article.
See Resources for more information on this topic.

简单的理解上边这段话,使用volatile有可能能解决问题,volatile被定义用来保正一致性,但是很多虚拟机
并没有很好的实现volatile,所以使用它也会存在问题。

最终的解决方案:
 (1),单态模式2,使用同步方法
 (2),放弃同步,使用一个静态变量,如下
class Singleton
{
  private Vector v;
  private boolean inUse;
  private static Singleton instance = new Singleton();

  private Singleton()
  {
    v = new Vector();
    inUse = true;
    //...
  }

  public static Singleton getInstance()
  {
    return instance;
  }
}
但使用静态变量也会存在问题,问题见 这篇文章

而且如在文章开头提到的,使用EJB跨服务器,跨JVM的情况下,单态更是问题

好了是不是感觉单态模式根本没法使用了,其实上边都是特殊情况,这中特殊情况的出现是有条件的,只要
根据你的具体应用,回避一些,就能解决问题,所以单态还是可以使用的。但是在使用前慎重,自己考虑好自己
的情况适合哪种情况。

参考
http://www.jdon.com/designpatterns/singleton.htm

Double-checked locking and the Singleton pattern

When is a singleton not a singleton?

posted on 2006-11-04 09:26 dreamstone 阅读(8661) 评论(12)  编辑  收藏 所属分类: 设计模式

评论

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2006-12-19 14:51 vg

四,为了对应上边的问题:单态模式4


-----------------------
我想请教一下,
既然被synchronized,一个线程没执行完,另一个线程是无法进入的.
就不会存在instance已经被new,已经分配了内存空间,但是没有初始化数据的情况

synchronized(Singleton.class) { //1
if (instance == null) //2
instance = new Singleton(); //3
}
  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2006-12-19 15:38 vg

看错了,看成double-checked 了  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2006-12-19 21:42 slx

double-check也是会出问题的,java中的double-check基本不是确定安全的,所以在使用的时候要格外小心,之所以很多人喜欢double check 是因为设计模式一书中讲单态的时候是double-check,但是因为设计模式是以c++为基础的,在c++中可以实现的,java不行,原因见我的文章中写的,是因为虚拟机的实现原理,如果英语好的话可以看一下写文章时我参考的几篇文章。  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2006-12-19 21:51 wueddie

”但使用静态变量也会存在问题“
我看了引用的文章,并不能说明静态变量的方式有问题。
文章只是说明了对象初始化的过程和要注意的问题。  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2007-02-27 17:39 dreamstone

to wueddie:
如果一个使用方法存在着各种各样的陷阱,就说明它存在问题。呵呵。我并不是说不能用,而是说在用的时候慎重。
  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2007-02-27 17:41 dreamstone

另外附加一段转载来的内容,转载自:
http://www.jdon.com/jive/thread.jsp?forum=91&thread=23890&message=14071631#14071631

Kyle_Yin 写的部分,本来想只写个链接,不过从众多回文中找也不是个好事情,所以转载过来这一段。

阎宏描述的原因是确切的。

不仅如此(构造函数与 instance 变量赋值的顺序完全取决于编译器实现),编译器产生的代码也可能被CPU再次做乱序和并行(out-of-order / parallelism)优化。现代CPU,包括各款奔腾在内,都使用乱序和并行来尽量避免流水线等待内存,IO,以提高执行效率。

另外,一个像样的CPU,是不会把那个 TEMP 写到内存里,再回头花费几百个 CPU CLOCK 从内存里拿回来,赋值给 m_instance 寄存器,然后再把 m_instance 写回内存的。其实远远不到 CPU 的层面,编译器已经会发现这样很不核算从而通通优化调。即使编译器不优化,CPU也不会这么做,而是把m_instance写到缓存里。既然牵扯到缓存,如果你有两个以上的物理CPU,或者两个以上的物理内核,或者两个以上的虚拟内核(所谓超线程),就不能保证各个执行单元看到的是一个值,除非用 synchronized{} 告诉 JVM 需要作 同步/缓存提交。

总而言之,在 JAVA 5 之前的JAVA 内存模型下,如果使用优化编译器,或者使用了并行架构的硬件,那么DCL是“不保证正确”的,无论用什么办法修改,就好像永动机不可能实现一样。

好消息是:包含在 JAVA 5 的 JSR 133 之下,如果把 m_instance 改为 volatile 变量,那么DCL至少是“功能正确”的。因为 JSR 133 更改了 volatile 的语义,使 volatile 变量的读写与 synchronized 有相当的涵义。

坏消息是:如果把 m_instance 改为 volatile 变量,那么DCL虽然功能正确,效率却和在 synchronized block 内部单一检查完全一样,甚至更差一点(因为检查了两次)。所以,DCL 就完全不具有物理意义了。

再次总而言之,DCL 在 JAVA 是没有价值的。值得一提的是,DCL在 C# 里也同样没有意义。抄的就是抄的,连 BUG 都一起抄去了。呵呵。

不过老板关于SPRING的评价在这里并不合乎语境。如果我的需求必须计较 SYNCHRONIZED 所带来的几百个 CPU CLOCK 的性能损失,又怎么可能容忍 XML PARSING 和 REFLECTION?   回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2007-02-27 22:37 dreamstone

今天终于发现了一种写法可以试用,具体看这里:
http://www.blogjava.net/dreamstone/archive/2007/02/27/101000.html  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2007-02-28 17:28 dreamstone

这里有许多关于单态的讨论,观点是尽量不使用单态,而使用IOC容器
http://www.jdon.com/jive/thread.jsp?forum=91&thread=17578  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2007-05-22 13:48 passager

@wueddie
你说的对.用静态变量是可以实现的.
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2007-05-23 10:44 dreamstone

@passager
我的意思并不是说静态变量不可以实现。
而是静态变量的实现也存在一些问题,特别是存在一些陷阱,这样就造成一个问题,当第一个人写完了是没有问题的,但随着需求的复杂,后边也许会换别人来维护这个东西,如果代码中有陷阱,就说明第一个人留下的并不是一个好的方法。因为按照常理来说,维护代码的可用时间比开发时要赶的多。更容易出错。  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break)[未登录] 2012-12-08 22:51 eric

你好,请问一下

synchronized(Singleton.class) {

if(instance == null) {

instance = new Singleton();

}
}



这段代码不是说明了若线程A取得锁,执行实例化的操作的话,线程B必须要等到线程A执行完这段代码后并释放锁后才能进入同步块吧?是不是说线程A中只取得了把单例对象的地址赋值给instance而没有执行对象的init方法就把锁释放了让线程B可以取得锁?  回复  更多评论   

# re: Java中的模式 --单态 (部分翻译 double-checked locking break) 2013-07-04 11:29 wyt

这种写法是可靠的,但是性能会比较低。  回复  更多评论   


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


网站导航: