现在更好的 Java 单例模式? [复制]
Posted
技术标签:
【中文标题】现在更好的 Java 单例模式? [复制]【英文标题】:The better Java singleton pattern nowadays? [duplicate] 【发布时间】:2011-08-14 22:27:06 【问题描述】:你知道,自从 Java 5 发布以来,在 Java 中编写单例模式的推荐方法是使用枚举。
public enum Singleton
INSTANCE;
但是,我不喜欢这样 - 强制客户端使用 Singleton.INSTANCE 才能访问单例实例。 也许,将 Singleton 隐藏在普通类中的更好方法,并提供对 Singleton 设施的更好访问:
public class ApplicationSingleton
private static enum Singleton
INSTANCE;
private ResourceBundle bundle;
private Singleton()
System.out.println("Singleton instance is created: " +
System.currentTimeMillis());
bundle = ResourceBundle.getBundle("application");
private ResourceBundle getResourceBundle()
return bundle;
private String getResourceAsString(String name)
return bundle.getString(name);
;
private ApplicationSingleton()
public static ResourceBundle getResourceBundle()
return Singleton.INSTANCE.getResourceBundle();
public static String getResourceAsString(String name)
return Singleton.INSTANCE.getResourceAsString(name);
所以,客户端现在可以简单地写:
ApplicationSingleton.getResourceAsString("application.name")
例如。 那么哪个更好:
Singleton.INSTANCE.getResourceAsString("application.name")
所以,问题是:这样做是否正确?此代码是否有任何问题(线程安全?)?它是否具有“枚举单例”模式的所有优点?似乎它需要两个世界中的更好。你怎么看?有没有更好的方法来实现这一目标? 谢谢。
编辑 @所有 首先,在 Effective Java,第 2 版中提到了 Singleton 模式的枚举用法:wikipedia:Java Enum Singleton。我完全同意我们应该尽可能减少 Singleton 的使用,但我们不能完全放弃它们。 在我提供另一个示例之前,让我说,ResourceBundle 的第一个示例只是一个案例,示例本身(和类名)并非来自真实应用程序。但是,需要说明的是,我不知道 ResourceBundle 缓存管理,感谢您提供的信息)
下面,Singleton 模式有两种不同的方法,第一种是 Enum 的新方法,第二种是我们大多数人以前使用的标准方法。我试图展示它们之间的显着差异。
使用枚举的单例: ApplicationSingleton 类是:
public class ApplicationSingleton implements Serializable
private static enum Singleton
INSTANCE;
private Registry registry;
private Singleton()
long currentTime = System.currentTimeMillis();
System.out.println("Singleton instance is created: " +
currentTime);
registry = new Registry(currentTime);
private Registry getRegistry()
return registry;
private long getInitializedTime()
return registry.getInitializedTime();
private List<Registry.Data> getData()
return registry.getData();
;
private ApplicationSingleton()
public static Registry getRegistry()
return Singleton.INSTANCE.getRegistry();
public static long getInitializedTime()
return Singleton.INSTANCE.getInitializedTime();
public static List<Registry.Data> getData()
return Singleton.INSTANCE.getData();
注册表类是:
public class Registry
private List<Data> data = new ArrayList<Data>();
private long initializedTime;
public Registry(long initializedTime)
this.initializedTime = initializedTime;
data.add(new Data("hello"));
data.add(new Data("world"));
public long getInitializedTime()
return initializedTime;
public List<Data> getData()
return data;
public class Data
private String name;
public Data(String name)
this.name = name;
public String getName()
return name;
和测试类:
public class ApplicationSingletonTest
public static void main(String[] args) throws Exception
String rAddress1 =
ApplicationSingleton.getRegistry().toString();
Constructor<ApplicationSingleton> c =
ApplicationSingleton.class.getDeclaredConstructor();
c.setAccessible(true);
ApplicationSingleton applSingleton1 = c.newInstance();
String rAddress2 = applSingleton1.getRegistry().toString();
ApplicationSingleton applSingleton2 = c.newInstance();
String rAddress3 = applSingleton2.getRegistry().toString();
// serialization
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(applSingleton1);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
ApplicationSingleton applSingleton3 = (ApplicationSingleton) in.readObject();
String rAddress4 = applSingleton3.getRegistry().toString();
List<Registry.Data> data = ApplicationSingleton.getData();
List<Registry.Data> data1 = applSingleton1.getData();
List<Registry.Data> data2 = applSingleton2.getData();
List<Registry.Data> data3 = applSingleton3.getData();
System.out.printf("applSingleton1=%s, applSingleton2=%s, applSingleton3=%s\n", applSingleton1, applSingleton2, applSingleton3);
System.out.printf("rAddr1=%s, rAddr2=%s, rAddr3=%s, rAddr4=%s\n", rAddress1, rAddress2, rAddress3, rAddress4);
System.out.printf("dAddr1=%s, dAddr2=%s, dAddr3=%s, dAddr4=%s\n", data, data1, data2, data3);
System.out.printf("time0=%d, time1=%d, time2=%d, time3=%d\n",
ApplicationSingleton.getInitializedTime(),
applSingleton1.getInitializedTime(),
applSingleton2.getInitializedTime(),
applSingleton3.getInitializedTime());
这是输出:
Singleton instance is created: 1304067070250
applSingleton1=ApplicationSingleton@18a7efd, applSingleton2=ApplicationSingleton@e3b895, applSingleton3=ApplicationSingleton@6b7920
rAddr1=Registry@1e5e2c3, rAddr2=Registry@1e5e2c3, rAddr3=Registry@1e5e2c3, rAddr4=Registry@1e5e2c3
dAddr1=[Registry$Data@1dd46f7, Registry$Data@5e3974], dAddr2=[Registry$Data@1dd46f7, Registry$Data@5e3974], dAddr3=[Registry$Data@1dd46f7, Registry$Data@5e3974], dAddr4=[Registry$Data@1dd46f7, Registry$Data@5e3974]
time0=1304067070250, time1=1304067070250, time2=1304067070250, time3=1304067070250
应该说什么:
-
单例实例只创建一次
是的,ApplicationSingletion 有几个不同的实例,但它们都包含相同的 Singleton 实例
所有不同ApplicationSingleton实例的注册表内部数据都是相同的
所以,总结一下:Enum 方法工作正常,可以防止通过反射攻击创建重复的 Singleton,并在序列化后返回相同的实例。
使用标准方法的单例: ApplicationSingleton 类是:
public class ApplicationSingleton implements Serializable
private static ApplicationSingleton INSTANCE;
private Registry registry;
private ApplicationSingleton()
try
Thread.sleep(10);
catch (InterruptedException ex)
long currentTime = System.currentTimeMillis();
System.out.println("Singleton instance is created: " +
currentTime);
registry = new Registry(currentTime);
public static ApplicationSingleton getInstance()
if (INSTANCE == null)
return newInstance();
return INSTANCE;
private synchronized static ApplicationSingleton newInstance()
if (INSTANCE != null)
return INSTANCE;
ApplicationSingleton instance = new ApplicationSingleton();
INSTANCE = instance;
return INSTANCE;
public Registry getRegistry()
return registry;
public long getInitializedTime()
return registry.getInitializedTime();
public List<Registry.Data> getData()
return registry.getData();
Registry 类是(请注意,Registry 和 Data 类应显式实现 Serializable 以使序列化工作):
//now Registry should be Serializable in order serialization to work!!!
public class Registry implements Serializable
private List<Data> data = new ArrayList<Data>();
private long initializedTime;
public Registry(long initializedTime)
this.initializedTime = initializedTime;
data.add(new Data("hello"));
data.add(new Data("world"));
public long getInitializedTime()
return initializedTime;
public List<Data> getData()
return data;
// now Data should be Serializable in order serialization to work!!!
public class Data implements Serializable
private String name;
public Data(String name)
this.name = name;
public String getName()
return name;
ApplicationSingletionTest 类是(大体相同):
public class ApplicationSingletonTest
public static void main(String[] args) throws Exception
String rAddress1 =
ApplicationSingleton.getInstance().getRegistry().toString();
Constructor<ApplicationSingleton> c =
ApplicationSingleton.class.getDeclaredConstructor();
c.setAccessible(true);
ApplicationSingleton applSingleton1 = c.newInstance();
String rAddress2 = applSingleton1.getRegistry().toString();
ApplicationSingleton applSingleton2 = c.newInstance();
String rAddress3 = applSingleton2.getRegistry().toString();
// serialization
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(byteOut);
out.writeObject(applSingleton1);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(byteOut.toByteArray()));
ApplicationSingleton applSingleton3 = (ApplicationSingleton) in.readObject();
String rAddress4 = applSingleton3.getRegistry().toString();
List<Registry.Data> data = ApplicationSingleton.getInstance().getData();
List<Registry.Data> data1 = applSingleton1.getData();
List<Registry.Data> data2 = applSingleton2.getData();
List<Registry.Data> data3 = applSingleton3.getData();
System.out.printf("applSingleton1=%s, applSingleton2=%s, applSingleton3=%s\n", applSingleton1, applSingleton2, applSingleton3);
System.out.printf("rAddr1=%s, rAddr2=%s, rAddr3=%s, rAddr4=%s\n", rAddress1, rAddress2, rAddress3, rAddress4);
System.out.printf("dAddr1=%s, dAddr2=%s, dAddr3=%s, dAddr4=%s\n", data, data1, data2, data3);
System.out.printf("time0=%d, time1=%d, time2=%d, time3=%d\n",
ApplicationSingleton.getInstance().getInitializedTime(),
applSingleton1.getInitializedTime(),
applSingleton2.getInitializedTime(),
applSingleton3.getInitializedTime());
这是输出:
Singleton instance is created: 1304068111203
Singleton instance is created: 1304068111218
Singleton instance is created: 1304068111234
applSingleton1=ApplicationSingleton@16cd7d5, applSingleton2=ApplicationSingleton@15b9e68, applSingleton3=ApplicationSingleton@1fcf0ce
rAddr1=Registry@f72617, rAddr2=Registry@4f1d0d, rAddr3=Registry@1fc4bec, rAddr4=Registry@1174b07
dAddr1=[Registry$Data@1256ea2, Registry$Data@82701e], dAddr2=[Registry$Data@1f934ad, Registry$Data@fd54d6], dAddr3=[Registry$Data@18ee9d6, Registry$Data@19a0c7c], dAddr4=[Registry$Data@a9ae05, Registry$Data@1dff3a2]
time0=1304068111203, time1=1304068111218, time2=1304068111234, time3=1304068111218
应该说什么:
-
已创建多个单例实例!次
所有注册表对象都是具有自己数据的不同对象
所以,总结一下:标准方法对于反射攻击是弱的,并且在被序列化后返回不同的实例,但是对于相同的数据是可以的。
因此,Enum 方法似乎更加可靠和稳健。它是当今在 Java 中使用单例模式的推荐方式吗?你怎么看? 有趣的事实要解释:为什么枚举中的对象可以使用其拥有的类进行序列化,但没有实现 Serializable?是功能还是错误?
【问题讨论】:
最好的单身人士是不存在的单身人士:) 同意 Bozho,尤其是在这种情况下使用ResourceBundle
对象。 ResourceBundle
已经缓存了以前检索到的包,因此调用getBundle("bundle.name")
将检索现有的。然后像往常一样使用该对象
ResourceBundles 为了您的方便已经被缓存了,也许您可以避免所有的单例问题并相信已经在其中实现的缓存机制。
您应该减少问题中的详细说明。您的问题现在比问题更多。只需说明推荐的来源以及推荐原因即可。
【参考方案1】:
我不知道这些天枚举是 Java 构建单例的方式。但是如果你打算这样做,你还不如直接使用枚举。我看不出有什么好的理由将单例封装在一堆静态成员方法后面;完成此操作后,您不妨先编写一个带有私有静态成员的静态类。
【讨论】:
同意,我认为封装这样的枚举没有任何好处。最终,私有构造函数和静态方法应该为您提供所需的线程安全。将它们封装成这样的枚举似乎只是添加了不必要的封装层,而没有添加任何功能。再说一次,我也不知道人们建议以这种方式使用枚举,所以我可能会忽略一些东西。就我个人而言,我会完全跳过枚举并使用你已经拥有的私有构造函数和静态方法。 @Jberg:使用枚举的优势在于它隐含地保护了单例免受多线程访问、序列化和反射的影响——否则你必须手动实现这些事情(并且涉及关于双线程的无休止的争论)检查锁定)【参考方案2】:“更好”的单例模式是不要使用一个。
您描述的方法,就像所有通过静态初始化创建单例的方法一样,非常难以调试。
改为使用依赖注入(有或没有 Spring 等框架)。
【讨论】:
那你如何确保一个类实例只创建一次而没有框架检查呢?【参考方案3】:我看到这种方法的问题是代码重复;如果你的单例有很多方法,你最终会写两次以确保你的委托逻辑有效。查看“initialization on demand holder idiom”以替代您的方法,该方法是线程安全的并且不需要枚举破解。
【讨论】:
使用 enum 不是 hack - 它更干净、更容易并且保证可以工作。 @Michael:IMO 这是一个 hack,因为它不是为了更安全的单例创建而引入的;碰巧的是,JVM 处理enum
s 的方式使它们成为单例模式的理想候选者。另外,如果您最终因单例而面临并发/数据共享问题,您总是可以让getInstance()
返回一个新实例,而这在enum
s 中是不可能的。当然,YMMV。
@MichaelBorgwardt - 为此目的使用枚举是“违背意图的编程”【参考方案4】:
[...] 推荐的写作方式 Java中的单例模式正在使用 枚举 [...]
老实说,我不知道这个建议来自哪里,但它肯定是有缺陷的。最重要的是因为 Java 中枚举的序列化与普通类的序列化完全不同。
当一个枚举被序列化时,只有它的名字被写入流中,基本上是因为预期枚举的性质是完全静态的。枚举反序列化后,根据Enum.valueOf(name)重新构建。
这意味着如果您将枚举用作单例,并且如果您的单例不是完全静态的,命名它具有动态状态,那么如果您将它序列化,那么您将遇到一些有趣的错误。
这意味着枚举并不总是解决方案,尽管有时它们可能是一个好方法。
似乎您想要完成的是确保 ResourceBundle 的唯一实例,不确定拥有两个实例是否会以任何可能的方式影响您的应用程序,但无论如何,ResourceBundle 是由JDK 已经缓存了资源包实例。
Javadocs 说:
默认缓存所有加载的资源包。
这意味着如果您尝试两次获取相同的资源包,您将获得相同的实例,前提是缓存尚未失效:
ResourceBundle resource1 = ResourceBundle.getBundle("test");
ResourceBundle resource2 = ResourceBundle.getBundle("test");
assert resource1==resource2;
如果您打算节省一些内存,那么您不需要单例机制。提供的缓存可以为您解决问题。
我不是这方面的专家,但是如果您查看 ResourceBundle Javadocs,也许您可以找到一种更好的方法来处理资源包,而不是在这个枚举单例中。
【讨论】:
目前的想法似乎是无状态的单例可以作为枚举很好地处理,而有状态的单例最好通过依赖注入来处理。 @Andrew Lazarus 你现在的想法是什么意思?我的意思是,我想看看这一切是从哪里来的。 带状态的枚举是不好的,因为你给出的原因(对 de 序列化,也许还有其他人感到非常惊讶。一般来说,关于单例的争论很大,它们有真正的缺点测试,序列化等。但是我没有听到任何反对 stateless Singletons 的好论据,并且这些作为枚举得到了很好的处理。在无状态的情况下,默认的 Enum 序列化(例如)正在做正是你想要的。【参考方案5】:我需要感谢你关于这次谈话,但我需要将私有构造函数代码更新为:
private ApplicationSingleton()
long currentTime = System.currentTimeMillis();
System.out.println("Singleton instance is created: " + currentTime);
这是输出:
Singleton instance is created: 1347981459285
Singleton instance is created: 1347981459285
Singleton instance is created: 1347981459285
applSingleton1=singlton.enums.ApplicationSingleton@12bc8f01,
applSingleton2=singlton.enums.ApplicationSingleton@3ae34094,
applSingleton3=singlton.enums.ApplicationSingleton@1da4d2c0
应该说什么:
-
已创建多个单例实例!次
所有注册表对象都是具有自己数据的不同对象
因为我们强制私有构造函数在 c.setAccessible(true);
值为 true 表示反射对象在使用时应禁止 Java 语言访问检查。值为 false 表示反射对象应强制执行 Java 语言访问检查。
因此,要测试单例模式的线程安全性,您应该使用多线程应用程序
【讨论】:
【参考方案6】:约书亚·布洛赫 (Joshua Bloch) 在他的书 Effective Java 中推广了单例的枚举方法。另一个好方法是lazy holder pattern,这有点类似于 OP 的想法。我认为将枚举隐藏在 OP 提议的类中不会增加任何性能或并发风险。
Singleton 仍然被大量使用,尽管它们通常隐藏在我们使用的框架中。是否使用单例取决于具体情况,我不同意永远不要使用它们。由于在一些设计不佳的系统中可怕的过度使用,Singleton 的名声不好。
【讨论】:
【参考方案7】:我喜欢 Singleton 的枚举,但是当你需要继承时(就像我一样 here)。枚举不能继承。 使用dp4j,最小的单例看起来像这样:
@com.dp4j.Singleton //(lazy=false)
public class MySingleton extends Parent
dp4j 实际上会创建这个:
@Singleton
public class ApplicationSingleton extends Parent
@instance
private static ApplicationSingleton instance = new ApplicationSingleton();
private ApplicationSingleton()
@getInstance
public static ApplicationSingleton getInstance()
return instance;
正如您所指出的,此解决方案容易受到“反射攻击”的影响。在dp4j.com 上确实有一个关于如何使用反射 API 对单例进行单元测试的演示。
【讨论】:
@see ***.com/questions/5427818/…以上是关于现在更好的 Java 单例模式? [复制]的主要内容,如果未能解决你的问题,请参考以下文章