【问题标题】:Lock-free and wait-free thread-safe lazy initialization无锁和无等待线程安全延迟初始化
【发布时间】:2015-07-27 17:28:06
【问题描述】:

要执行无锁和无等待延迟初始化,我执行以下操作:

private AtomicReference<Foo> instance = new AtomicReference<>(null);  

public Foo getInstance() {
   Foo foo = instance.get();
   if (foo == null) {
       foo = new Foo();                       // create and initialize actual instance
       if (instance.compareAndSet(null, foo)) // CAS succeeded
           return foo;
       else                                   // CAS failed: other thread set an object 
           return instance.get();             
   } else {
       return foo;
   }
}

而且它工作得很好,除了一件事:如果两个线程看到实例null,它们都会创建一个新对象,只有一个幸运的是通过CAS操作设置它,这会导致资源浪费。

是否有人建议另一种 无锁 惰性初始化模式,它可以降低两个并发线程创建两个昂贵对象的可能性?

【问题讨论】:

  • 实际上,没有像“无锁线程安全”这样的东西,这在逻辑上是不可能的——因为即使是“从不等待”的复杂工作(重新)调度程序实际上也需要至少一个内部的、瞬态的& 透明甚至不可见的锁,以保持数据一致并防止损坏。但是等待/锁定的数量可以减少到非常很小且有效的最小值,这是正确的。
  • @specializt 通过停放线程锁定与在 CAS 中锁定非常不同......
  • 天啊@specializt。术语无锁是众所周知的并且被广泛使用。在这种情况下,没有理由争论这些无关紧要的问题。
  • @SashaSalauyou 你对你的赏金有什么期望?当前的答案缺少什么?

标签: java multithreading lazy-initialization lock-free wait-free


【解决方案1】:

如果你想要真正的无锁,你将不得不做一些旋转。您可以拥有一个线程“赢得”创建权,但其他线程必须旋转直到准备好。

private AtomicBoolean canWrite = new AtomicBoolean(false);  
private volatile Foo foo; 
public Foo getInstance() {
   while (foo == null) {
       if(canWrite.compareAndSet(false, true)){
           foo = new Foo();
       }
   }
   return foo;
}

这显然存在忙于旋转的问题(您可以在其中设置 sleep 或 yield),但我可能仍会推荐 Initialization on demand

【讨论】:

  • 嗯,这很有趣,尽管它不是免等待的。我也应该测试一下。
  • @SashaSalauyou 是的,我的意思是使用 AtomicBoolean
  • @SashaSalauyou 你是对的,它不是免等待的。
  • 阅读en.wikipedia.org/wiki/Non-blocking_algorithm#Wait-freedom。在谈论并发算法时,Wait-free 具有特定的含义。也就是说,如果保证操作在有限数量的步骤中完成,则并发算法是无等待的。在 OP 的示例中,最大步数为 2。
  • 在我的示例中,不知道非实例化线程的操作需要多长时间。实际上它显然不会很长,但是在不知道线程将旋转多少次的情况下,我们失去了无等待属性。这就是我们在无阻塞、无锁和无等待之间进行推理的方式。
【解决方案2】:

我认为您需要为对象创建本身进行一些同步。我会这样做:

// The atomic reference itself must be final!
private final AtomicReference<Foo> instance = new AtomicReference<>(null);
public Foo getInstance() {
  Foo foo = instance.get();
  if (foo == null) {
    synchronized(instance) {
      // You need to double check here
      // in case another thread initialized foo
      Foo foo = instance.get();
      if (foo == null) {
        foo = new Foo(); // actual initialization
        instance.set(foo);
      }
    }
  }
  return foo;
}

这是一种非常常见的模式,尤其是对于懒惰的单身人士。 Double checked locking 最小化了 synchronized 块的实际执行次数。

【讨论】:

  • 谢谢,我知道这种方法,但现在我正在寻找无锁的方法:)
  • 请问为什么?替代方案(旋转,使用额外的原子变量)不是很吸引 IMO 并使事情变得如此复杂。
  • 使用原子引用是因为它在其他代码段中被 CAS 化了。
  • 我的意思是你为什么想要无锁初始化。
  • 实现完全无锁定和无等待。我同意,这个问题不是很实用。好奇心。
【解决方案3】:

我可能会使用惰性初始化单例模式:

private Foo() {/* Do your heavy stuff */}

private static class CONTAINER {
 private static final Foo INSTANCE = new Foo();
}

public static Foo getInstance() {
 return CONTAINER.INSTANCE;
}

我实际上没有看到任何理由为自己使用 AtomicReference 成员字段。

【讨论】:

【解决方案4】:

如果使用另一个volatile 变量来锁定呢? 你可以用新变量做双锁吗?

【讨论】:

  • 它有什么帮助?请举个例子。
【解决方案5】:

我不确定最终结果是否应该以性能为中心,如果是,下面不是解决方案。例如,您能否检查两次,然后在第一次检查后调用 thread.sleep 方法随机毫秒小于 100 毫秒。

private AtomicBoolean canWrite = new AtomicBoolean(false);  
private volatile Foo foo; 
public Foo getInstance() {
   if(foo==null){
          Thread.Sleep(getRandomLong(50)) // you need to write method for it
         if(foo==null){
            foo = new Foo();
      }
   }
   return foo;
}

【讨论】:

  • 下面我正在阅读您对 Yuri 的评论,“Thread.sleep() 导致上下文切换”。你能帮我理解一下吗,我想它会解决我的老问题。 ithread.sleep 通过阻止线程处理帮助我控制内存泄漏。但仍然在某个休息日我得到内存异常。
  • 您的问题非常笼统,超出了此处的讨论范围。我想您应该先阅读有关 java 内存模型和垃圾收集的内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-11-17
  • 1970-01-01
  • 2014-07-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-16
相关资源
最近更新 更多