双重检查锁定文章

11

我在阅读 这篇文章,讲的是关于“双重检查锁定”的内容。除了文章的主题之外,我想知道为什么作者在文章的某个地方使用了下面的习惯用语:

清单7. 尝试解决乱序写问题

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;
}
我的问题是:同步一些使用相同锁的代码两次有什么原因吗?这样做有什么目的吗?
非常感谢您提前的帮助。

有一个相关的问题"Java中最佳Singleton实现"。 - Stephen Denne
10个回答

15

两次锁定的目的是为了尝试防止乱序写入。内存模型规定了重排序可能发生的位置,部分取决于锁。锁确保在“instance = inst;”这一行之后没有写入(包括在单例构造函数中的任何写入)。

然而,要深入研究该主题,我建议参考Bill Pugh的文章。但最好永远不要尝试这样做 :)


但是您可以在文章末尾使用实际工作的Java 5变体。 - Hans-Peter Störr
1
@hstoerr:个人而言,除非我发现它是某些代码中的实际瓶颈,否则我不会使用它。这太容易出错了。当然,每个东西都有其适用的场合,但我很少使用它。 - Jon Skeet

13
文章涉及到Java的内存模型(JMM)5.0之前的版本。在该模型下,离开同步块会强制将写操作刷新到主内存。因此,似乎是为了确保单例对象被推出并在引用它之前进行实例化。但是,它并不完全起作用,因为对实例的写入可能会被移动到块中 - 蟑螂旅馆。
然而,5.0之前的模型从未被正确实现。1.4应该遵循5.0模型。类是惰性初始化的,所以你可以直接写:
public static final Singleton instance = new Singleton();

或者更好的方法是,不要使用单例,因为它们是邪恶的。


6

Jon Skeet是正确的:阅读Bill Pugh的文章。Hans使用的习语是精确的形式,但是不起作用,不应该使用。

这是不安全的:

private static Singleton instance;

public static Singleton getInstance() {
  if (instance == null) {
    synchronized(Singleton.class) {
      if (instance == null) {
        instance = new Singleton();
      }
    }
  }
  return instance;
}

这也是不安全的:

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;
}

不要做这两件事,永远不要。
相反,同步整个方法:
    public static synchronized Singleton getInstance() {
      if (instance == null) {
        instance = new Singleton();
      }
      return instance;
    }

除非您每秒钟检索此对象一百万次,否则实际上的性能影响可以忽略不计。


3

1

遵循John Skeet的建议:

然而,为了更深入地了解这个主题,我建议阅读Bill Pugh的文章。然后永远不要尝试它 :)

这里是第二个同步块的关键:

这段代码将Helper对象的构造放在内部同步块中。直观的想法是,在释放同步时应该有一个内存屏障,并且应该防止对Helper对象的初始化和对helper字段的赋值进行重新排序。

因此,基本上,通过内部同步块,我们试图“欺骗”JMM在同步块内创建实例,以强制JMM在同步块完成之前执行该分配。但是问题在于,JMM正在向我们靠近,并将在同步块之前的分配移动到同步块内,将我们的问题移回到开始。

这就是我从这些文章中理解的内容,非常有趣,再次感谢您的回复。


0

请观看Google Tech Talk上的Java内存模型,以了解JMM更细微的要点。由于这里缺少,我还想指出Jeremy Manson的博客'Java并发编程',特别是关于双重检查锁定的文章(在Java世界中任何有价值的人似乎都会写一篇关于它的文章:)。


0

0
关于这个习语,有一篇非常值得推荐和阐明的文章:

http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.html?page=1

另一方面,我认为dhighwayman.myopenid的意思是,为什么作者在一个同步块中引用了同一个类(synchronized(Singleton.class)),而在另一个同步块中也引用了同一个类。这可能是因为在该块内创建了一个新实例(Singleton inst = instance;),为了保证其线程安全性,需要编写另一个同步块。
否则,我看不出任何意义。

0

对于Java 5及更高版本,实际上有一种双重检查变量的方式,可以比同步整个访问器更好。这也在Double-Checked Locking Declaration中提到:

class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        if (helper == null) {
            synchronized(this) {
                if (helper == null)
                    helper = new Helper();
            }
        }
        return helper;
    }
}

这里的关键区别在于变量声明中使用了volatile - 否则它不起作用,并且在Java 1.4或更低版本中也无法工作。


0

好的,但文章说:

清单7中的代码由于当前内存模型的定义而无法工作。Java语言规范(JLS)要求在同步块内的代码不得移出同步块。但是,它没有说不在同步块中的代码不能移入同步块。

另外似乎JVM将下一个转换为ASM中的"伪代码":

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;
}

到目前为止,“instance=inst”之后没有写入的点还没有完成吗?

我现在会阅读这篇文章,感谢提供链接。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接