初始化可能失败的惰性单例的哪种实现?
想象一下,您有一个静态无参方法,它是幂等的并且始终返回相同的值,并且可能会抛出检查异常,如下所示:
class Foo {
public static Pi bar() throws Baz { getPi(); } // gets Pi, may throw
}
现在,如果构造返回对象的内容很昂贵,那么这是惰性单例的良好候选者并且永远不会改变。一种选择是 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 仍然是一种反模式吗?我这里还缺少其他解决方案吗(也可以使用诸如活泼的单一检查之类的小变体,但不会从根本上改变解决方案)?有充分的理由选择其中之一吗?
我没有尝试上面的例子,所以它们完全有可能无法编译。
编辑:我没有能力重新实现或重新架构这个单例的消费者(即Foo.bar()
的调用者),也没有我有机会介绍一个DI框架来解决这个问题吗?我最感兴趣的是在给定的约束内解决问题的答案(提供一个带有传播给调用者的已检查异常的单例)。
更新:我最终决定选择 DCL,因为它提供了保留现有合同的最干净的方式,并且没有人提供为什么应该避免正确执行 DCL 的具体原因。我没有在接受的答案中使用该方法,因为它似乎是实现相同目标的一种过于复杂的方法。
Imagine you have a static no-argument method which is idempotent and always returns the same value, and may throw a checked exception, like so:
class Foo {
public static Pi bar() throws Baz { getPi(); } // gets Pi, may throw
}
Now this is a good candidate for a lazy singleton, if the stuff that constructs the returned Object is expensive and never changes. One choice would be the Holder pattern:
class Foo {
static class PiHolder {
static final Pi PI_SINGLETON = getPi();
}
public static Pi bar() { return PiHolder.PI_SINGLETON; }
}
Unfortunately, this won't work because we can't throw a checked exception from an (implicit) static initializer block, so we could instead try something like this (assuming we want to preserve the behavior that the caller gets the checked exception when they call 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;
}
}
At this point, maybe double-checked locking is just cleaner?
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;
}
}
Is DCL still an anti-pattern? Are there other solutions I'm missing here (minor variants like racy single check are also possible, but don't fundamentally change the solution)? Is there a good reason to choose one over the other?
I didn't try the examples above so it's entirely possible that they don't compile.
Edit: I don't have the luxury of re-implementing or re-architecting the consumers of this singleton (i.e., the callers of Foo.bar()
), nor do I have the opportunity to introduce a DI framework to solve this problem. I'm mostly interested in answers which solve the issue (provision of a singleton with checked exceptions propagated to the caller) within the given constraints.
Update: I decided to go with DCL after all, since it provided the cleanest way to preserve the existing contract, and no one provided a concrete reason why DCL done properly should be avoided. I didn't use the method in the accepted answer since it seemed just to be a overly complex way of achieving the same thing.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
“Holder”技巧本质上是由 JVM 执行的双重检查锁定。根据规范,类初始化处于(双重检查)锁定状态。 JVM 可以安全(快速)地执行 DCL,不幸的是,Java 程序员无法获得这种能力。我们能做的最接近的是通过中间最终参考。请参阅有关 DCL 的维基百科。
您保留异常的要求并不难:
The "Holder" trick is essentially double checked locking performed by JVM. Per spec, class initialization is under (double checked) locking. JVM can do DCL safely (and fast), unfortunately, that power isn't available to Java programmers. The closest we can do, is through a intermediary final reference. See wikipedia on DCL.
Your requirement of preserving exception isn't hard:
我强烈建议扔掉单例和可变静态。 “正确使用构造函数。”构造对象并将其传递给需要它的对象。
i strongly suggest throwing out the Singleton and mutable statics in general. "Use constructors correctly." Construct the object and the pass it into objects that need it.
根据我的经验,当您尝试获取的对象需要的不仅仅是简单的构造函数调用时,最好使用依赖项注入。
...或者如果惰性很重要:(
具体细节在某种程度上取决于您的 DI 框架)
您可以告诉 DI 框架将对象绑定为单例。从长远来看,这为您提供了更大的灵活性,并使您的类更易于进行单元测试。
此外,我的理解是,由于 JIT 编译器可能对指令进行重新排序,Java 中的双重检查锁定不是线程安全的。编辑:正如 Meron 指出的那样,双重检查锁定可以在 Java 中工作,但您必须使用 volatile 关键字。最后一点:如果您使用良好的模式,通常很少或没有理由希望您的类被延迟实例化。最好让您的构造函数非常轻量,并将大部分逻辑作为方法的一部分执行。我并不是说在这种特殊情况下你一定做错了什么,但你可能想更广泛地了解如何使用这个单例,看看是否没有更好的方法来构建事物。
In my experience, it's best to go with dependency injection when the object you're trying to get requires anything more than a simple constructor call.
... or if Laziness is important:
(the specifics will depend somewhat on your DI framework)
You can tell the DI framework to bind the object as a singleton. This gives you a lot more flexibility in the long run, and makes your class more unit-testable.
Also, my understanding is that double-checked locking in Java is not thread safe, due to the possibility of instruction reordering by the JIT compiler.edit: As meriton points out, double-checked locking can work in Java, but you must use the volatile keyword.One last point: if you're using good patterns, there's usually little or no reason to want your classes to be lazily instantiated. It's best to have your constructor be extremely light-weight, and have the bulk of your logic be performed as part of the methods. I'm not saying you're necessarily doing something wrong in this particular case, but you may want to take a broader look at how you're using this singleton and see if there's not a better way to structure things.
由于您没有告诉我们您需要什么,因此很难提出更好的实现方法。我可以告诉你,懒惰的单例很少是最好的方法。
不过,我可以看到您的代码有几个问题:
您如何期望字段访问抛出异常?
编辑:正如 Irreputable 所指出的,如果访问导致类初始化,并且初始化因静态初始化器抛出异常而失败,那么您实际上会在这里得到 ExceptionInInitializerError 。但是,VM 在第一次失败后不会尝试再次初始化该类,并使用不同的异常进行通信,如以下代码所示:
结果:
而不是 ExceptionInInitializerError。
您的双重检查锁定也遇到类似的问题;如果构造失败,则该字段保持为空,并且每次访问 PI 时都会重新尝试构造
PI
。如果失败是永久性的并且代价高昂,您可能希望采取不同的做法。Since you don't tell us what you need this for it's kind of hard to suggest better ways of achieving it. I can tell you that lazy singletons are rarely the best approach.
I can see several issues with your code, though:
Just how do you expect a field access to throw an exception?
Edit: As Irreputable points out, if the access causes class initialization, and initialization fails because the static initializer throws an exception, you actually get the ExceptionInInitializerError here. However, the VM will not try to initialized the class again after the first failure, and communicate this with a different exception, as the following code demonstrates:
Results in:
and not an ExceptionInInitializerError.
Your double checked locking suffers from a similar problem; if construction fails, the field remains null, and a new attempt to construct the
PI
is made every time PI is accessed. If failure is permanent and expensive, you might wish to do things differently.