Java单例模式实例化

10

我发现了三种单例模式的实例化方法,但我对它们中是否有最佳方法存在疑虑。我正在多线程环境下使用它们,并且希望采用延迟初始化。
示例1:

private static final ClassName INSTANCE = new ClassName();

public static ClassName getInstance() {
    return INSTANCE;
}

示例2:

private static class SingletonHolder { 
    public static final ClassName INSTANCE = new ClassName();
}

public static ClassName getInstance() {
    return SingletonHolder.INSTANCE;
}

示例 3:

private static ClassName INSTANCE;

public static synchronized ClassName getInstance()
{
    if (INSTANCE == null)
        INSTANCE = new ClassName();

    return INSTANCE;
}

我正在使用的项目在各处都使用Sample 2,但我更喜欢Sample 3。还有枚举版本,但我不太明白它。

问题是,在哪些情况下我应该/不应该使用这些变体?虽然我不想要冗长的解释(关于这个问题已经有很多其他话题了,但它们最终都变成了争论),我希望用简短的话说明白。

4个回答

20

在Java中实现单例最安全、最简单的方法是使用枚举(Enum),就像你提到的那样:

public enum ClassName {
    INSTANCE;

    // fields, setters and getters
}

枚举语义保证只有一个INSTANCE

如果不使用枚举方法,您必须注意许多方面,例如竞态条件和反射。我曾经破坏过某些框架的单例,并对它们进行了滥用,因为它们编写不当。枚举保证没有人会破坏它。


但是我该怎么使用它呢?例如,我有这个:public class MyClass { public enum ClassName { INSTANCE; // 我应该把所有的方法都放在这里吗,而不是像平常一样放在下面? } // 还是像平常一样放在下面? }编辑:该死,注释没有格式化??讨厌! - jurchiks
1
请在下面编写您的单例字段和方法。然后通过ClassName.INSTANCE.doSomething(foo)访问它们。 - Bozho
@Carl,类加载是懒加载的。只有在加载类但不想加载实例时才会出现问题。你不能独立地加载它们。 - Peter Lawrey
如果您希望方法的调用是线程安全的,则将它们设置为synchronized - Bozho
如何在枚举中实例化主类或在getInstance()上需要抛出异常? - jurchiks
显示剩余7条评论

5

示例1没有使用惰性初始化。

示例2和3都是惰性的。 示例2使用初始化时等待持有人习惯用语(IODH),它没有同步开销。因此比示例3更快。

在《Effective Java》(第3项)中,Joshua Bloch建议单元素枚举类型是实现单例的最佳方法

但是,如果您对枚举类型不确定,请坚持使用IODH。


为什么示例1没有使用延迟初始化?我认为它在使用,因为只有一个实例,并且进行了一次初始化。 - Sam003

3
首先,请务必确保您需要一个单例,并且希望为单例提供“全局级别”的访问。我发现,在许多情况下,单例的客户端不需要知道它是单例。相反,他们只需要在实例化时获得服务。
因此,无论您如何获取单例(如果有),请考虑更改类获取此对象的方式。尽管这意味着修改构造函数并更改“分发更改”,但我发现依赖注入框架可以降低成本。 DI框架还可以负责单例实例化(例如,Guice就是这样做的)。
除此之外,选项3是我熟悉的典型和最常见的(线程安全)版本。选项1主要用于非惰性初始化(并非总是可接受)。我从未见过使用选项2。

我想实例化的类中包含了两个Map对象,可能有大量数据,每个Map都有两个方法,可以使用键获取值(并在值为空时填充值),还有两个方法来访问数据库(第一个是为所选键填充值,第二个是使用现有数据向数据库插入/更新/删除数据)。这里不计算getter/setter。您认为它不应该是单例吗? - jurchiks
我不确定你是否真正理解了我所提出的核心观点。假设你将刚才描述的一切都封装成一个名为DBLookupService和DBLookupServiceImpl的接口及其对应的类中。对于使用LookupService的类来说,它不需要关心它来自哪里,重要的是获得LookupService的实例。这也使得测试变得更加容易。 - Uri
关于是否应该使用单例模式 - 可能是的,但您可以考虑多核优化代码,在其中使用多个实例(例如,如果您可以有意义地划分数据)。但我的观点是,您应该尽一切可能避免从使用服务的代码中调用getInstance()。这将使测试和未来更改变得更加容易。 - Uri

2

示例1:如果您不需要懒惰单例,请使用它。
示例2:永远不要使用 - 太多类会让人感到困惑。而仅用于保存一个变量的内部类似乎有点不必要。 示例3:这是一个懒惰的单例。如果需要,请使用它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接