在Java中实现单例设计模式的有效方法是什么?
当前回答
根据用法,有几个“正确”答案。
自Java5以来,最好的方法是使用枚举:
public enum Foo {
INSTANCE;
}
在Java 5之前,最简单的情况是:
public final class Foo {
private static final Foo INSTANCE = new Foo();
private Foo() {
if (INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
return INSTANCE;
}
public Object clone() throws CloneNotSupportedException{
throw new CloneNotSupportedException("Cannot clone instance of this class");
}
}
让我们复习一下代码。首先,你希望这门课是期末考试。在本例中,我使用了final关键字来让用户知道它是final。然后,需要将构造函数设为私有,以防止用户创建自己的Foo。从构造函数中抛出异常会阻止用户使用反射来创建第二个Foo。然后创建一个私有的静态final Foo字段来保存唯一的实例,并创建一个公共的静态Foo-getInstance()方法来返回它。Java规范确保只有在首次使用类时才调用构造函数。
当您有一个非常大的对象或大量的构造代码,并且还有其他可访问的静态方法或字段,这些方法或字段可能在需要实例之前使用,那么只有在需要时才需要使用惰性初始化。
您可以使用私有静态类来加载实例。代码将如下所示:
public final class Foo {
private static class FooLoader {
private static final Foo INSTANCE = new Foo();
}
private Foo() {
if (FooLoader.INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
return FooLoader.INSTANCE;
}
}
由于行private static final Foo INSTANCE=new Foo();仅在实际使用FooLoader类时执行,这会处理延迟实例化,并保证线程安全。
当您还希望能够序列化对象时,需要确保反序列化不会创建副本。
public final class Foo implements Serializable {
private static final long serialVersionUID = 1L;
private static class FooLoader {
private static final Foo INSTANCE = new Foo();
}
private Foo() {
if (FooLoader.INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
return FooLoader.INSTANCE;
}
@SuppressWarnings("unused")
private Foo readResolve() {
return FooLoader.INSTANCE;
}
}
方法readResolve()将确保返回唯一的实例,即使对象是在程序的上一次运行中序列化的。
其他回答
我对一些答案感到困惑,这些答案建议依赖注入(DI)作为使用单态的替代方案;这些都是不相关的概念。您可以使用DI注入单例或非单例(例如,每个线程)实例。至少如果您使用Spring2.x,这是正确的,我不能为其他DI框架说话。
所以我对OP的回答是(除了最简单的示例代码之外):
使用类似Spring framework的DI框架,然后将其作为DI配置的一部分,无论依赖项是单体的、请求范围的、会话范围的还是其他。
这种方法为您提供了一个很好的解耦(因此是灵活和可测试的)架构,其中是否使用单例是一个容易可逆的实现细节(当然,前提是您使用的任何单例都是线程安全的)。
忘记延迟初始化;这太有问题了。这是最简单的解决方案:
public class A {
private static final A INSTANCE = new A();
private A() {}
public static A getInstance() {
return INSTANCE;
}
}
根据用法,有几个“正确”答案。
自Java5以来,最好的方法是使用枚举:
public enum Foo {
INSTANCE;
}
在Java 5之前,最简单的情况是:
public final class Foo {
private static final Foo INSTANCE = new Foo();
private Foo() {
if (INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
return INSTANCE;
}
public Object clone() throws CloneNotSupportedException{
throw new CloneNotSupportedException("Cannot clone instance of this class");
}
}
让我们复习一下代码。首先,你希望这门课是期末考试。在本例中,我使用了final关键字来让用户知道它是final。然后,需要将构造函数设为私有,以防止用户创建自己的Foo。从构造函数中抛出异常会阻止用户使用反射来创建第二个Foo。然后创建一个私有的静态final Foo字段来保存唯一的实例,并创建一个公共的静态Foo-getInstance()方法来返回它。Java规范确保只有在首次使用类时才调用构造函数。
当您有一个非常大的对象或大量的构造代码,并且还有其他可访问的静态方法或字段,这些方法或字段可能在需要实例之前使用,那么只有在需要时才需要使用惰性初始化。
您可以使用私有静态类来加载实例。代码将如下所示:
public final class Foo {
private static class FooLoader {
private static final Foo INSTANCE = new Foo();
}
private Foo() {
if (FooLoader.INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
return FooLoader.INSTANCE;
}
}
由于行private static final Foo INSTANCE=new Foo();仅在实际使用FooLoader类时执行,这会处理延迟实例化,并保证线程安全。
当您还希望能够序列化对象时,需要确保反序列化不会创建副本。
public final class Foo implements Serializable {
private static final long serialVersionUID = 1L;
private static class FooLoader {
private static final Foo INSTANCE = new Foo();
}
private Foo() {
if (FooLoader.INSTANCE != null) {
throw new IllegalStateException("Already instantiated");
}
}
public static Foo getInstance() {
return FooLoader.INSTANCE;
}
@SuppressWarnings("unused")
private Foo readResolve() {
return FooLoader.INSTANCE;
}
}
方法readResolve()将确保返回唯一的实例,即使对象是在程序的上一次运行中序列化的。
实现单例有很多细微差别。夹持器图案不能在许多情况下使用。在使用volatile时,也应该使用局部变量。让我们从头开始,反复讨论这个问题。你会明白我的意思的。
第一次尝试可能看起来像这样:
public class MySingleton {
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
...
}
这里我们有一个MySingleton类,它有一个名为INSTANCE的私有静态成员和名为getInstance()的公共静态方法。第一次调用getInstance()时,INSTANCE成员为空。然后,流将进入创建条件,并创建MySingleton类的新实例。对getInstance()的后续调用将发现INSTANCE变量已设置,因此不会创建另一个MySingleton实例。这确保只有一个MySingleton实例在getInstance()的所有调用方之间共享。
但这种实现有一个问题。多线程应用程序在创建单个实例时具有竞争条件。如果多个执行线程同时(或前后)命中getInstance()方法,它们将分别将INSTANCE成员视为null。这将导致每个线程创建一个新的MySingleton实例,然后设置instance成员。
private static MySingleton INSTANCE;
public static synchronized MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
这里我们在方法签名中使用了synchronized关键字来同步getInstance()方法。这肯定会修复我们的种族状况。线程现在将一次一个地阻塞并进入方法。但这也造成了性能问题。这个实现不仅同步了单个实例的创建;它同步对getInstance()的所有调用,包括读取。读取不需要同步,因为它们只需返回INSTANCE的值。由于读取将构成我们调用的大部分(记住,实例化只发生在第一次调用上),因此通过同步整个方法,我们将导致不必要的性能损失。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronize(MySingleton.class) {
INSTANCE = new MySingleton();
}
}
return INSTANCE;
}
在这里,我们将同步从方法签名移到了一个同步块,该块包装MySingleton实例的创建。但这能解决我们的问题吗?嗯,我们不再阻止读取,但我们也后退了一步。多个线程将同时或几乎同时命中getInstance()方法,它们都会将INSTANCE成员视为空。
然后,它们将到达同步块,在那里将获得锁并创建实例。当该线程退出块时,其他线程将争夺锁,每个线程将逐一通过块并创建我们类的新实例。所以我们回到了我们开始的地方。
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
在这里,我们从区块内部发出另一张支票。如果INSTANCE成员已经设置,我们将跳过初始化。这称为双重检查锁定。
这解决了我们的多重实例化问题。但我们的解决方案再次提出了另一个挑战。其他线程可能无法“看到”INSTANCE成员已更新。这是因为Java如何优化内存操作。
线程将变量的原始值从主内存复制到CPU的缓存中。然后,对值的更改将写入该缓存并从中读取。这是Java的一个旨在优化性能的特性。但这给我们的单例实现带来了一个问题。第二个线程 — 由不同的CPU或内核使用不同的缓存进行处理 — 不会看到第一个所做的更改。这将导致第二个线程将INSTANCE成员视为空,从而强制创建单例的新实例。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
我们通过在INSTANCE成员的声明上使用volatile关键字来解决这个问题。这将告诉编译器始终读取和写入主内存,而不是CPU缓存。
但这种简单的改变是有代价的。因为我们绕过了CPU缓存,所以每次对易失性INSTANCE成员进行操作时,我们都会受到性能影响 — 我们做了四次。我们再次检查存在性(1和2),设置值(3),然后返回值(4)。有人可能会说,这条路径是边缘情况,因为我们只在方法的第一次调用期间创建实例。也许创作上的表现是可以容忍的。但即使是我们的主要用例reads,也会对volatile成员进行两次操作。一次检查是否存在,再次返回其值。
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
MySingleton result = INSTANCE;
if (result == null) {
synchronized(MySingleton.class) {
result = INSTANCE;
if (result == null) {
INSTANCE = result = createInstance();
}
}
}
return result;
}
由于性能受到影响是由于直接对volatile成员进行操作,所以让我们将一个局部变量设置为volatile的值,并改为对局部变量进行操作。这将减少我们在易失性上操作的次数,从而收回一些损失的性能。注意,当我们进入同步块时,必须再次设置本地变量。这确保了它是最新的,以及在我们等待锁时发生的任何更改。
我最近写了一篇关于这个的文章。解构单身汉。您可以在那里找到有关这些示例和“持有者”模式示例的更多信息。还有一个真实的例子展示了双重检查的易失性方法。
在写它之前,真的要考虑一下为什么你需要一个单例。关于使用它们有一个准宗教的争论,如果你在Java中搜索单例,你很容易就会发现。
就我个人而言,出于许多原因,我尽量避免单身汉,其中大部分可以通过谷歌搜索单身汉找到。我觉得单身汉经常被虐待,因为他们很容易被每个人理解。它们被用作将“全局”数据引入OO设计的机制,因为它很容易绕过对象生命周期管理(或者真正思考如何从B内部实现a)。看看像控制反转(IoC)或依赖注入(DI)这样的事情,可以找到一个不错的中间地带。
如果你真的需要一个,那么Wikipedia有一个很好的例子来说明单例的正确实现。
推荐文章
- 在流中使用Java 8 foreach循环移动到下一项
- 访问限制:'Application'类型不是API(必需库rt.jar的限制)
- 用Java计算两个日期之间的天数
- 如何配置slf4j-simple
- 在Jar文件中运行类
- 带参数的可运行?
- 我如何得到一个字符串的前n个字符而不检查大小或出界?
- 我可以在Java中设置enum起始值吗?
- Java中的回调函数
- c#和Java中的泛型有什么不同?和模板在c++ ?
- 在Java中,流相对于循环的优势是什么?
- Jersey在未找到InjectionManagerFactory时停止工作
- 在Java流是peek真的只是调试?
- Recyclerview不调用onCreateViewHolder
- 将JSON字符串转换为HashMap