2

想象一下,你有一个静态的无参数方法,它是幂等的并且总是返回相同的值,并且可能会抛出一个检查异常,如下所示:

class Foo {
 public static Pi bar() throws Baz { getPi(); } // gets Pi, may throw 
}

现在,如果构造返回的 Object 的东西很昂贵并且永远不会改变,那么这对于惰性单例来说是一个很好的候选者。一种选择是 Holder 模式:

class Foo {
  static class PiHolder {
   static final Pi PI_SINGLETON = getPi();
  }
  public static Pi bar() { return PiHolder.PI_SINGLETON; }
}

不幸的是,这行不通,因为我们不能从(隐式)静态初始化程序块中抛出已检查异常,因此我们可以尝试这样的事情(假设我们希望保留调用者在他们获得已检查异常时的行为)打电话bar()):

class Foo {
  static class PiHolder {
   static final Pi PI_SINGLETON;
   static { 
    try { 
     PI_SINGLETON =  = getPi(); }
    } catch (Baz b) {
     throw new ExceptionInInitializerError(b);
    }
  }

  public static Pi bar() throws Bar {
   try {
    return PiHolder.PI_SINGLETON;
   } catch (ExceptionInInitializerError e) {
    if (e.getCause() instanceof Bar)
     throw (Bar)e.getCause();
    throw e;
   }
}

在这一点上,也许双重检查锁定更干净?

class Foo {
 static volatile Pi PI_INSTANCE;
 public static Pi bar() throws Bar {
  Pi p = PI_INSTANCE;
  if (p == null) {
   synchronized (this) {
    if ((p = PI_INSTANCE) == null)
     return PI_INSTANCE = getPi();
   }
  }
  return p;
 }
}

DCL 仍然是反模式吗?我在这里是否缺少其他解决方案(也可以使用诸如 racy single check 之类的小变体,但不要从根本上改变解决方案)?有充分的理由选择其中一个吗?

我没有尝试上面的示例,所以它们完全有可能无法编译。

编辑: 我没有重新实现或重新构建这个单例的消费者(即调用者Foo.bar())的奢侈,也没有机会引入一个 DI 框架来解决这个问题。我最感兴趣的是在给定约束内解决问题的答案(提供带有已检查异常的单例传播给调用者)。

更新:毕竟我决定使用 DCL,因为它提供了保存现有合同的最干净的方式,并且没有人提供应该避免正确执行 DCL 的具体原因。我没有在接受的答案中使用该方法,因为它似乎只是实现同一目标的一种过于复杂的方法。

4

4 回答 4

1

我强烈建议一般扔掉单例和可变静态。“正确使用构造函数。” 构造对象并将其传递给需要它的对象。

于 2011-03-02T20:58:42.963 回答
1

根据我的经验,当您尝试获取的对象需要的不仅仅是简单的构造函数调用时,最好使用依赖注入。

public class Foo {
  private Pi pi;
  public Foo(Pi pi) {
    this.pi = pi;
  }
  public Pi bar() { return pi; }
}

...或者如果懒惰很重要:

public class Foo {
  private IocWrapper iocWrapper;
  public Foo(IocWrapper iocWrapper) {
    this.iocWrapper = iocWrapper;
  }
  public Pi bar() { return iocWrapper.get(Pi.class); }
}

(具体情况将在一定程度上取决于您的 DI 框架)

您可以告诉 DI 框架将对象绑定为单例。从长远来看,这为您提供了更多的灵活性,并使您的课程更具单元测试性。

另外,我的理解是,Java 中的双重检查锁定不是线程安全的,因为 JIT 编译器可能会重新排序指令。 编辑:正如meriton 指出的那样,双重检查锁定可以在Java 中工作,但您必须使用 volatile 关键字。

最后一点:如果您使用了良好的模式,通常很少或没有理由希望您的类被延迟实例化。最好让您的构造函数非常轻量级,并将大部分逻辑作为方法的一部分执行。我并不是说在这种特殊情况下你一定做错了什么,但你可能想更广泛地看看你是如何使用这个单例的,看看是否没有更好的方法来构建事物。

于 2011-03-02T21:00:27.307 回答
1

“Holder”技巧本质上是由 JVM 执行的双重检查锁定。根据规范,类初始化处于(双重检查)锁定状态。JVM 可以安全(且快速)地执行 DCL,不幸的是,Java 程序员无法获得这种能力。我们能做的最接近的是通过中间的最终参考。请参阅有关 DCL 的维基百科。

您保留异常的要求并不难:

class Foo {
  static class PiHolder {
    static final Pi PI_SINGLETON;
    static Bar exception;
    static { 
      try { 
        PI_SINGLETON =  = getPi(); }
      } catch (Bar b) {
        exception = b;
      }
    }
  }
public Pi bar() throws Bar {
  if(PiHolder.exception!=null)
    throw PiHolder.exception;  
  else
    return PiHolder.PI_SINGLETON;
}
于 2011-03-02T21:01:07.890 回答
1

由于您没有告诉我们您需要什么,因此很难提出更好的方法来实现它。我可以告诉你,惰性单例很少是最好的方法。

不过,我可以看到您的代码存在几个问题:

try {
    return PiHolder.PI_SINGLETON;
} catch (ExceptionInInitializerError e) {

您如何期望字段访问引发异常?


编辑:正如 Irreputable 指出的那样,如果访问导致类初始化,并且由于静态初始化程序引发异常而初始化失败,那么您实际上会在此处获得 ExceptionInInitializerError。但是,VM 不会在第一次失败后再次尝试初始化该类,并与不同的异常进行通信,如以下代码所示:

static class H {
    final static String s; 
    static {
        Object o = null;
        s = o.toString();
    }
}

public static void main(String[] args) throws Exception {
    try {
        System.out.println(H.s);
    } catch (ExceptionInInitializerError e) {
    }
    System.out.println(H.s);
}

结果是:

Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class tools.Test$H 
        at tools.Test.main(Test.java:21)

而不是 ExceptionInInitializerError。


您的双重检查锁定遇到了类似的问题;PI如果构造失败,则该字段保持为空,并且每次访问 PI 时都会进行一次新的构造尝试。如果失败是永久性的并且代价高昂,您可能希望以不同的方式做事。

于 2011-03-02T21:15:58.993 回答