现在更好的Java单例模式?

您知道,自Java 5发布以来,在Java中编写Singleton模式的推荐方法是使用枚举。

public enum Singleton { INSTANCE; } 

但是,我不喜欢这个 – 是强制客户端使用Singleton.INSTANCE来访问单例实例。 也许,更好的方法是在普通类中隐藏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") 

所以,问题是:这是正确的方法吗? 此代码是否有任何问题(线程安全?)? 它具有“enum singleton”模式的所有优点吗? 它似乎从两个世界中取得了更好的成绩。 你怎么看? 有没有更好的方法来实现这一目标? 谢谢。

编辑
@所有
首先,在Effective Java,第2版: 维基百科:Java Enum Singleton中提到了Singleton模式的枚举用法。 我完全同意我们应该尽可能地减少Singleton的使用,但我们不能完全放弃它们。
在我提供另一个示例之前,让我说,使用ResourceBundle的第一个示例只是一个案例,示例本身(和类名称)不是来自实际应用程序。 但是,需要说的是,我不了解ResourceBundle缓存管理,感谢这条信息)

下面,Singleton模式有两种不同的方法,第一种是使用Enum的新方法,第二种是我们大多数人之前使用过的标准方法。 我试图表明它们之间的显着差异。

单身人士使用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 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 getData() { return Singleton.INSTANCE.getData(); } } 

注册表类是:

 public class Registry { private List data = new ArrayList(); 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 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 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 data = ApplicationSingleton.getData(); List data1 = applSingleton1.getData(); List data2 = applSingleton2.getData(); List 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 

应该提一下:

  1. 单例实例仅创建一次
  2. 是的,ApplicationSingletion有几个不同的实例,但它们都包含相同的Singleton实例
  3. 注册表内部数据对于所有不同的 ApplicationSingleton实例都是相同的

因此,总结一下 :Enum方法工作正常,可以防止通过reflection攻击创建重复的Singleton,并在序列化后返回相同的实例。

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 getData() { return registry.getData(); } } 

注册表类是(注意,注册表和数据类显式应该实现Serializable,以便序列化工作):

 //now Registry should be Serializable in order serialization to work!!! public class Registry implements Serializable { private List data = new ArrayList(); 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 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 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 data = ApplicationSingleton.getInstance().getData(); List data1 = applSingleton1.getData(); List data2 = applSingleton2.getData(); List 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 

应该提一下:

  1. Singleton实例创建了几个! 时
  2. 所有注册表对象都是具有自己数据的不同对象

因此,总结一下 :标准方法对于reflection攻击来说很弱,并且在序列化后返回不同的实例,但是对于相同的数据则是。


因此,似乎Enum方法更加稳固和强大。 现在是在Java中使用Singleton模式的推荐方法吗? 你怎么看?
有趣的事实要解释:为什么enum中的对象可以使用其拥有的类进行序列化并不实现Serializable? 是function还是bug?

我不知道枚举是这些天构建单身人士的Java方式。 但如果你打算这样做,你也可以直接使用枚举。 我没有看到任何有理由将单例封装在一堆静态成员方法之后; 一旦你完成了这个,你也可以编写一个私有静态成员的静态类。

“更好”的单身人士模式不是使用一个。

您描述的方法,如通过静态初始化创建单例的所有方法,都非常难以调试。

相反,使用dependency injection(使用或不使用Spring等框架)。

[…]在Java中编写Singleton模式的推荐方法是使用枚举[…]

老实说,我不知道这个建议来自哪里,但肯定存在缺陷。 最重要的是因为Java中的枚举序列化与普通类的序列化完全不同。

当序列化枚举时,只将其名称写入流中,主要是因为预期枚举的性质完全是静态的。 当enum被反序列化时,它将基于Enum.valueOf(name)再次构建。

这意味着如果你使用枚举作为单例,并且你的单例不是完全静态的,命名它具有动态状态,那么如果你序列化它,那么你就会遇到一些有趣的错误。

这意味着枚举并不总是解决方案,尽管有时它们可​​能是一种很好的方法。

看来你想要完成的是确保ResourceBundle的唯一实例,不确定它的两个实例是否会以任何可能的方式影响你的应用程序,但无论如何,它已经由JDK实现的ResourceBundle缓存资源包实例。

Javadocs说:

默认情况下,所有加载的资源包都会被缓存。

这意味着如果您尝试两次获取相同的资源包,则会获得相同的实例,前提是缓存尚未失效:

 ResourceBundle resource1 = ResourceBundle.getBundle("test"); ResourceBundle resource2 = ResourceBundle.getBundle("test"); assert resource1==resource2; 

如果您打算节省一些内存,那么您不需要单例机制。 提供的缓存可以为您提供帮助。

我不是这方面的专家,但是如果你看看ResourceBundle Javadocs,也许你可以找到一种更好的方法来处理资源包,而不是在这个enum singlenton中。

我用这种方法看到的问题是代码重复; 如果你的单身人士有很多方法,你最终会写两次以确保你的委托逻辑有效。 查看“ 按需初始化持有者习惯用法 ”,以替代您的方法,该方法是线程安全的,不需要枚举。

我需要感谢你对这个对话,但我需要更新私有构造函数代码:

 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 

应该提一下:

  1. Singleton实例创建了几个! 时
  2. 所有注册表对象都是具有自己数据的不同对象

因为我们强制私有构造函数在c.setAccessible(true)中公开;

值true表示reflection对象在使用时应禁止Java语言访问检查。 值false表示reflection对象应强制执行Java语言访问检查。

因此,要测试单例模式线程安全性,您应使用multithreading应用程序

单身人士的enum方法由Joshua Bloch在他的“ Effective Java”一书中推广。 另一个好方法是懒惰的持有者模式 ,这有点类似于OP的想法。 我认为在像OP提议的类中隐藏枚举不会增加任何性能或并发风险。

单身人士仍然使用很多,虽然他们经常隐藏在我们使用的框架内。 是否使用Singleton取决于具体情况,我不同意永远不会使用它们。 由于在一些设计不良的系统中过度使用,Singleton的名字很糟糕。

我喜欢Singleton的枚举,但是当你需要inheritance时(如我在这里所做的那样)show show。 枚举不能inheritance。 使用dp4j ,最小的Singleton看起来像这样:

 @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; } 

正如您所指出的,这种解决方案容易受到“reflection攻击”的影响。 在dp4j.com上,确实有一个关于如何使用Reflection API对Singleton进行unit testing的演示。