辛格尔顿仔细检查并发问题(Singleton double-check concurrency is

2019-06-23 21:05发布

休耕子句从jetbrains.net取读这篇文章,一些其他文章在网络上后,我还是不明白这怎么可能返回NULL,第一个线程进入到锁之后。 有一种人,不明白它可以请帮我和更人性化的方式解释一下吗?

“请考虑以下的代码:

public class Foo
{
  private static Foo instance;
  private static readonly object padlock = new object();

  public static Foo Get()
  {
    if (instance == null)
    {
      lock (padlock)
      {
        if (instance == null)
        {
          instance = new Foo();
        }
      }
    }
    return instance;
  }
};

鉴于上面的代码,写到初始化美孚实例可以被延迟,直到实例值的写入,从而产生该实例返回一个未初始化状态的对象的可能性。

为了避免这种情况,实例值必须进行波动。 “

Answer 1:

返回null不是问题。 问题是,新的实例可以在部分构造的状态被另一个线程所察觉。 考虑本声明Foo

class Foo
{
  public int variable1;
  public int variable2;

  public Foo()
  {
    variable1 = 1;
    variable2 = 2;
  }
}

下面是该代码可以由C#编译器,JIT编译器或硬件得到优化。 1

if (instance == null)
{
  lock (padlock)
  {
    if (instance == null)
    {
      instance = alloc Foo;
      instance.variable1 = 1; // inlined ctor
      instance.variable2 = 2; // inlined ctor
    }
  }
}
return instance;

首先,注意到构造函数是内联(因为它很简单)。 现在,希望这是很容易看到, instance被分配参考它的组成字段将会在构造函数中初始化之前。 这是一个有效的策略,因为读取和写入都是免费的,只要到上下浮动,因为他们没有通过的边界lock或改变的逻辑流程; 他们不知道。 因此,另一个线程可以看到instance != null ,并尝试使用它,它完全初始化之前。

volatile修复这个问题,因为它把阅读作为获取栅栏和写入作为隔离栅栏

  • 获得围栏:一种存储器屏障,其中其他读取及写入操作是不允许栅栏前移动。
  • 释放围栏:一种存储器屏障,其中其他读取及写入操作是不允许的围栏后移动。

所以,如果我们纪念instancevolatile则释放栅栏将防止上述优化。 下面是代码将如何看待与障碍物的注解。 我用了一个↑箭头指示释放围栏和↓箭头指示获取围栏。 请注意,没有什么是允许其过去的↑箭头或向上浮动过去一个↓箭头。 想想箭头为推动所有一切的。

var local = instance;
↓ // volatile read barrier
if (local == null)
{
  var lockread = padlock;
  ↑ // lock full barrier
  lock (lockread)
  ↓ // lock full barrier
  {
    local = instance;
    ↓ // volatile read barrier
    if (local == null)
    {
      var ref = alloc Foo;
      ref.variable1 = 1; // inlined ctor
      ref.variable2 = 2; // inlined ctor
      ↑ // volatile write barrier
      instance = ref;
    }
  ↑ // lock full barrier
  }
  ↓ // lock full barrier
}
local = instance;
↓ // volatile read barrier
return local;

到的组成部分变量的写入Foo仍然可以重新排序,但请注意,记忆障碍,现在阻止它们分配给后发生的instance 。 使用箭头为指导想象被允许和不允许各种不同的优化策略。 请记住,没有读取写入操作允许其过去的↑箭头或向上浮动过去一个↓箭头。

Thread.VolatileWrite就已经解决了这个问题,以及和可能的语言没有使用volatile像VB.NET关键字。 如果你看看如何VolatileWrite实现,你会看到这一点。

public static void VolatileWrite(ref object address, object value)
{
  Thread.MemoryBarrier();
  address = value;
}

现在,这似乎是在第一直觉。 毕竟,存储器屏障被放置在分配之前 。 怎么样让致力于主内存你问的分配? 这岂不是更正确放置分配之后的阻隔? 如果这是你的直觉告诉你那是错误的 。 你看记忆障碍是不严格有关获取“新鲜读”或“承诺写入”。 这是所有指令排序。 这是迄今为止混乱的最大来源我明白了。

这也可能是重要的一提的是Thread.MemoryBarrier实际生成一个完整的围栏屏障。 所以,如果我用箭头使用我上面的符号则是这样的。

public static void VolatileWrite(ref object address, object value)
{
  ↑ // full barrier
  ↓ // full barrier
  address = value;
}

因此从技术上讲调用VolatileWrite会比我们到一个写更多volatile场会做。 请记住, volatile未在VB.NET允许的例子,但VolatileWrite是BCL的开,以便它可以在其他语言中使用。


1 这种优化主要是理论上的。 在ECMA规范并在技术上允许,但微软CLI实现ECMA规范的对待所有写就好像它们释放栅栏语义了。 这可能是CLI中的另一种实现方式仍然可以尽管执行该优化。



Answer 2:

比尔·普格写关于这个问题的几篇文章,是关于这一主题的参考。

一个值得注意的参考是, “双检锁是残破的”宣言。

粗略地说,这里的问题是:

在mutlicore VM,由一个线程写入,直到达到同步障碍(或存储器栅栏)可能不是其他线程可见。 你可以阅读“ 内存壁垒:软件黑客硬件查看 ”这是关于此事的真正的好文章。

所以,如果一个线程初始化一个对象A一个字段a ,并存储在字段中的对象的参考ref另一个对象的B ,我们在内存中的两个“单元”: a ,和ref 。 除非线程会强制使用的存储栅栏变化的可见性变化这两个存储器位置也可能同时成为其他线程可见。

在Java中,同步可以被强制synchronized 。 这是昂贵的,并且可替代它来声明为字段volatile在这种情况下,改变该细胞总是对所有线程可见。

但是Java的4和5之间波动变化在Java 4的语义,您需要同时定义aref挥发性,对doulbe检查,在我所描述的示例工作。

这是不直观,大多数人只会设置ref挥发性。 因此,他们改变这一点,在Java中5+,如果挥发性字段被修改( ref )它触发其他字段的修改同步( a )。

编辑:我唯一的,现在你问C#看看,而不是Java ......我离开我的答案,因为也许是有用的还是。



文章来源: Singleton double-check concurrency issue