【问题标题】:Double-Checked Locking without creating objects不创建对象的双重检查锁定
【发布时间】:2013-07-31 14:34:59
【问题描述】:

我正在使用双重检查锁定 (DCL) 来避免在不需要时对对象进行同步。在我的情况下,我需要在某个缓冲区为空时进行同步,让“处理线程”等待“传递线程”再次通知它 - 否则,“处理线程”将在循环中运行而不做任何有用的事情。

两个线程共享这些对象:

Object bufferLock = new Object();
Queue<Command> commands = new ConcurrentLinkedQueue<>(); // thread safe!

线程 1(“传递线程” - 填充缓冲区):

while (true)
    Command command = readCommand();
    commands.add(command);
    synchronize (bufferLock){
        bufferLock.notify(); // wake up Thread 2 (if waiting)
    }
}

线程 2(“处理线程” - 清空缓冲区):

while (true){
    if (commands.peek() == null){ // not creating anything here
        synchronized (bufferLock){
            if (commands.peek() == null){ // also not creating anything
                bufferLock.wait();
            }
        }
    }
    Command command = commands.poll();
    processCommand(command);
}

现在,NetBeans 出现了一个关于 DCL 的警告,这让我更深入地研究了这个主题,因为我不知道 DCL 的概念 - 我刚刚开始自己​​使用它。

据我在网上阅读几篇文章了解到,使用此模式时存在 Java 错误,但所有示例都将其与延迟加载结合使用。在这些示例中,对象是在同步块中创建的。在我的同步代码中,我不创建对象。

我的代码不安全吗? NetBeans 在显示警告方面是否正确?请注意,NetBeans 之前有一个与 DCL 相关的bug,所以我有点困惑。

【问题讨论】:

  • 这与使用内置执行器相比如何?这包含了一个线程池(可以是一个)和 Runnable 队列来运行。如果你确信它比内置库做同样的事情更好,我只会写你自己的;)
  • 另一点:您似乎正在重新发明已经为队列实现的阻塞操作。为什么?
  • DCL 的 java 错误是否仅与对象创建有关(在单例模式中)?
  • @Heuster 是的,这绝对是关于 DCL 的误报。
  • 可以使用单线程的Executor,没问题。

标签: java multithreading synchronized


【解决方案1】:

您创建的模式(或者更确切地说,反模式!)并不严格构成双重检查锁定,这通常是指对象引用从 null 开始然后由需要引用的第一个线程实例化的情况它,仅在空检查后进行同步。在 Java 5 之前,严格来说,您无法在 Java 中正确地实现这一点(尽管由于大多数 JVM 的实现方式,您可能会意外地侥幸逃脱)。从 Java 5 开始,您可以使用它,但它基本上没有意义。 (您可能对我不久前写的article on double-checked locking in Java 感兴趣,它更详细地研究了这个问题。类加载器有效地内置了同步功能,适用于您确实需要 DCL 之类的极少数情况。)

现在,这都是顺便说一句。严格来说,您在这里所拥有的并不是真正的 DCL。

您确实遇到的问题是您正在尝试混合范式。 Java 并发库的存在理由通常是为了避免同步和等待/通知的低级锁定。所以你真正应该做的只是使用一些 BlockingQueue 的风格并使用它内置的阻塞行为。同样,在其他示例中,我可以向您推荐一些 material I have written on blocking queues

【讨论】:

  • 这对我来说似乎很清楚。这就是我的想法:DCL 是关于在多线程环境中创建单例对象,因为我找不到任何没有单例的 DCL(反模式)示例。正如 MarkoTopolnik 也提到的,BlockingQueue 接口是要走的路。我会调整我的代码——它会更干净、更易读,而且确实不需要关心低级同步。我接受了您的回答,因为它是最完整的 - 它回答了我最初的问题,并且您提供了最佳选择。
  • 我已阅读 Effective Java Ed。 2 到现在,一个延迟加载的单例可以很容易地通过使用一个内部类来实现,该类包含一个静态字段,这将是你的单例。这是因为类加载在JVM中是同步的,从而保证静态字段只能初始化一次,并且静态字段只有在内部类被访问时才被初始化,有效的实现了惰性。然后单例方法可以只返回 InnerClass.STATIC_FIELD。它有一个名字:Initialization-on-demand holder idiom。
  • 是的,实现单例的通常方法是让类加载器完成工作。我在回答中特别提到了您使用 DCL 的问题。但是,如果您要问的问题是“我如何在 Java 中实现单例类”,那么正如您所说,答案通常是使用在类的静态代码中初始化的静态 final 字段并让类加载器执行同步工作(因为类是延迟加载的)。
【解决方案2】:

请查看 Brian Goetz 撰写的“Java Concurrency in Practice”一书,第 16.2.4 节“双重检查锁定”。它解释了为什么 DCL 是错误的做法。

【讨论】:

  • 你读那本书是绝对正确的(我也想接触“Effective Java”)。我应该买它。
  • 几个月前买的,现在在第 12 章。感谢您的回答,我很期待第 16 章,因为我再次阅读了您的回答。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-08
相关资源
最近更新 更多