【问题标题】:Double checked locking in modern JVMs现代 JVM 中的双重检查锁定
【发布时间】:2012-02-15 09:08:13
【问题描述】:

我有一个类可能会在初始化期间抛出任何运行时异常。我希望这个类是一个单例,因为在内存中保留多个对象的成本很高。我在另一个类中使用该类。

我的用例如下:

  • 我必须使用Controller 的单个实例。
  • Parent 的每个实例都必须使用相同的 Controller 实例。
  • Controller 构造函数可能会抛出异常。
  • 如果实例化失败,我应该 稍后重试实例化。

所以当我尝试对Controller 执行“获取”操作时,我会检查我的控制器实例是否为null,如果是,我会尝试再次实例化它。

以下是我的代码:

class Parent
{
    private static volatile Controller controller;
    private static final Object lock = new Object();

    static
    {
        try
        {
            controller = new Controller();
        }
        catch(Exception ex)
        {
            controller = null;
        }
    }

    private Controller getController() throws ControllerInstantiationException
    {
        if(controller == null)
        {
            synchronized(lock)
            {
                if(controller == null)
                {
                    try
                    {
                        controller = new Controller();
                    }
                    catch(Exception ex)
                    {
                        controller = null;
                        throw new ControllerInstatntationException(ex);
                    }
                }
            }
        }
        return controller;
    }

    //other methods that uses getController() 
}

我的问题是,这段代码有问题吗?我在某处读到上面的代码在 JVM 1.4 或更早版本中会出现问题。你能提供参考/解决方案吗?请注意,我之所以问这个问题是因为互联网上有很多关于这个话题的混淆。

谢谢。

【问题讨论】:

  • 您使用的是 Java 1.4 或更低版本吗?

标签: java multithreading synchronization


【解决方案1】:

我相信它没有损坏,原因是 volatile 声明。但恕我直言,最好避免这样的代码。例如,无法保证此代码适用于 Java 8。还有另一种创建惰性单例的方法。我总是(几乎)使用这种方法。第一次在 Java Concurrency in Practice 一书中遇到它。

public class Singleton {
        private Singleton() { }

        private static class SingletonHolder { 
                public static final Singleton instance = new Singleton();
        }

        public static Singleton getInstance() {
                return SingletonHolder.instance;
        }
}

我不知道你在你的代码中做了什么,很难说,如何调整它。最直接的方法,简单地使用同步方法。您是否真的想使用双重检查锁定获得一些性能优势?同步方法有瓶颈吗?

【讨论】:

  • 我的瓶颈是getController() 方法。它被非常频繁地使用。所以我不想简化对getController() 的调用,除非没有其他选择。痛点也是new Controller() 构造函数。这可能会失败,如果确实如此,我需要在稍后再次调用 getController() 时再次重试实例化。
  • 您发布的代码 sn-p 将不起作用,因为它只支持一次实例化。如果第一次实例化失败怎么办?
  • @Umar,这个例子适用于 Java 1.4。如果您有 Java 5.0,使用 enum 会简单得多(请参阅我的回答)
  • @Peter Lawrey 同意,枚举是另一种创建单例的方法。我认为 OP 应该审查他的设计并选择另一种解决方案。对我来说很难想象,OP 在做什么
  • 持有者模式或枚举只有在保证初始化程序只运行一次时才有效。在这种情况下,OP 希望选择稍后再试,小心(正确使用volatile)在现代 JVM(具有 Java 内存模型保证的)上的双重检查锁定保证工作。
【解决方案2】:

唯一被破坏的是使示例比它需要的复杂得多。

你只需要一个枚举

// a simple lazy loaded, thread safe singleton.
enum Controller {
    INSTANCE
}

【讨论】:

  • 我的控制器不能是枚举。我无法改变它。其次,Controller() 构造函数可能会失败。我需要再试一次。枚举是否解决了我的第二个问题?我不这么认为。
  • 您可以更改构造函数,使其不会失败。我会对你为什么觉得无法改变它感兴趣。将可能失败的代码移到以后的访问方法中。
  • 我不拥有 Controller 类的代码。控制器类是服务的包装器。有时在初始化控制器时,服务可能很忙,因此控制器变得无用,因此可能会抛出 InstantiationException。
【解决方案3】:

使用AtomicBoolean(很像我建议的here)会更安全,并且允许在失败时重复尝试实例化。

public static class ControllerFactory {
  // AtomicBolean defaults to the value false.
  private static final AtomicBoolean creatingController = new AtomicBoolean();
  private static volatile Controller controller = null;

  // NB: This can return null if the Controller fails to instantiate or is in the process of instantiation by another thread.
  public static Controller getController() throws ControllerInstantiationException {
    if (controller == null) {
      // Stop another thread creating it while I do.
      if (creatingController.compareAndSet(false, true)) {
        try {
          // Can fail.
          controller = new Controller();
        } catch (Exception ex) {
          // Failed init. Leave it at null so we try again next time.
          controller = null;
          throw new ControllerInstantiationException(ex);
        } finally {
          // Not initialising any more.
          creatingController.set(false);
        }
      } else {
        // Already in progress.
        throw new ControllerInstantiationException("Controller creation in progress by another thread.");
      }
    }
    return controller;
  }

  public static class ControllerInstantiationException extends Exception {
    final Exception cause;

    public ControllerInstantiationException(Exception cause) {
      this.cause = cause;
    }

    public ControllerInstantiationException(String cause) {
      this.cause = new Exception(cause);
    }
  }

  public static class Controller {
    private Controller() {
    }
  }
}

【讨论】:

  • 在您的双重检查单例变体中,考虑到使用 AtomicBoolean,控制器是否也需要是 volatile 的?
  • @Καrτhικ - 是的。尽管实际上使用任何原子为所有变量建立了一个“发生在之前”的障碍,但并未指定如此 - 它仅针对特定原子指定。在上面的代码中,controller 可以在一个线程中设置为非 null,但如果未设置volatile,则更改可能永远不会波及其他线程。
【解决方案4】:

是的,Java 内存模型保证它可以在现代 JVM 上工作。请参阅The "Double-Checked Locking is Broken" Declaration 中的新 Java 内存模型下部分。

正如其他答案所指出的,有更简单的单例模式,使用 Holder 类或枚举。但是,在像您这样的情况下,如果第一次尝试失败,您希望允许尝试重新初始化多次,我相信使用 volatile 实例变量进行双重检查锁定是可以的。

【讨论】:

    【解决方案5】:

    这不是您问题的答案,但Double-Checked Locking is Broken 上的这篇著名文章很好地解释了为什么它在 java 1.4 或更早版本中被破坏。

    【讨论】:

    • 相反,那篇文章说它在现代 JVM(1.5 及更高版本)上工作,就像 OP 一样使用 volatile
    猜你喜欢
    • 1970-01-01
    • 2011-08-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多