The better Java singleton pattern nowadays?
你知道,自从Java 5发布以来,在Java中编写单模式的推荐方法就是使用EnUM。
1 2 3 | public enum Singleton { INSTANCE; } |
但是,我不喜欢这样做——强迫客户机使用singleton.instance来访问singleton实例。也许,将singleton隐藏在普通类中的更好方法,并提供更好的访问singleton设施的途径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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); } } |
号
因此,客户现在可以简单地写:
1 | ApplicationSingleton.getResourceAsString("application.name") |
例如。哪一个比:
1 | Singleton.INSTANCE.getResourceAsString("application.name") |
。
所以,问题是:这是正确的方法吗?此代码是否有任何问题(线程安全?)?它是否具有"枚举单例"模式所具有的所有优势?看来这需要两个世界的更好。你怎么认为?有没有更好的方法来实现这一点?谢谢。
编辑
@全部
首先,在有效的Java,第二版:维基百科:JavaEnm SuntLon中提到了单模式的枚举用法。我完全同意我们应该尽可能减少单件使用,但我们不能完全放弃它们。
在我提供另一个示例之前,让我说,第一个带有resourcebundle的示例只是一个例子,示例本身(以及类名)不是来自真实的应用程序。但是,需要说的是,我不知道ResourceBundle缓存管理,感谢您提供的信息)
下面,有两种不同的方法来处理单例模式,第一种是使用枚举的新方法,第二种是我们大多数人以前使用的标准方法。我试着展示他们之间的显著差异。
singleton使用枚举:
ApplicationSingleton类是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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(); } } |
注册表类是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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; } } } |
。
和测试等级:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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 ", applSingleton1, applSingleton2, applSingleton3); System.out.printf("rAddr1=%s, rAddr2=%s, rAddr3=%s, rAddr4=%s ", rAddress1, rAddress2, rAddress3, rAddress4); System.out.printf("dAddr1=%s, dAddr2=%s, dAddr3=%s, dAddr4=%s ", data, data1, data2, data3); System.out.printf("time0=%d, time1=%d, time2=%d, time3=%d ", ApplicationSingleton.getInitializedTime(), applSingleton1.getInitializedTime(), applSingleton2.getInitializedTime(), applSingleton3.getInitializedTime()); } } |
。
下面是输出:
1 2 3 4 5 | 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只创建了一次单一实例百万千克1百万千克1是的,有几个不同的applicationsingletion实例,但它们都包含相同的singleton实例百万千克1百万千克1对于所有不同的applicationsingleton实例,注册表内部数据都相同百万千克1
综上所述:枚举方法工作良好,通过反射攻击防止了重复的单例创建,并在序列化后返回相同的实例。
采用标准方法的单件:
ApplicationSingleton类是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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(); } } |
。
注册表类为(请注意,为了使序列化工作,注册表和数据类应显式实现可序列化):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | //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类(基本相同):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | 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 ", applSingleton1, applSingleton2, applSingleton3); System.out.printf("rAddr1=%s, rAddr2=%s, rAddr3=%s, rAddr4=%s ", rAddress1, rAddress2, rAddress3, rAddress4); System.out.printf("dAddr1=%s, dAddr2=%s, dAddr3=%s, dAddr4=%s ", data, data1, data2, data3); System.out.printf("time0=%d, time1=%d, time2=%d, time3=%d ", ApplicationSingleton.getInstance().getInitializedTime(), applSingleton1.getInitializedTime(), applSingleton2.getInitializedTime(), applSingleton3.getInitializedTime()); } } |
号
下面是输出:
1 2 3 4 5 6 7 | 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创建了几个单例实例!次百万千克1百万千克1所有注册表对象都是具有自己数据的不同对象百万千克1
综上所述:对于反射攻击,标准方法很弱,在序列化后返回不同的实例,但是对于相同的数据,是的。
因此,似乎枚举方法更为可靠和健壮。这是现在Java中使用单模式的推荐方法吗?你怎么认为?
解释一个有趣的事实:为什么枚举内的对象可以用其所属的类序列化,而不实现可序列化?它是特性还是缺陷?
我不知道,枚举是Java的方式来建立单身人士这些天。但如果您打算这样做,您也可以直接使用枚举。我看不出在一堆静态成员方法后面封装singleton的任何好的理由;一旦这样做了,您还可以编写一个静态类,从私有静态成员开始。
[...] the recommended way to write
Singleton pattern in Java is using
enum [...]
老实说,我不知道这个建议是从哪里来的,但它肯定有缺陷。最重要的是,Java中枚举的序列化与普通类的序列化完全不同。
当一个枚举被序列化时,只有它的名称被写入流中,这基本上是因为它的本质是完全静态的。反序列化枚举时,将根据Enum.ValueOf(Name)重新生成该枚举。
这意味着,如果将枚举用作单例,并且如果单例不是完全静态的,那么它实际上具有动态状态,那么如果将其序列化,那么您将发现一些有趣的错误。
这意味着Enums不能总是解决方案,尽管有时它们可能是一个好方法。
似乎您要完成的是确保ResourceBundle的唯一实例,不确定它有两个实例是否会以任何可能的方式影响您的应用程序,但无论如何,由JDK实现的ResourceBundle已经缓存了资源包实例。
爪哇人说:
默认情况下,加载的所有资源束都会缓存。
这意味着,如果您两次尝试获取相同的资源包,则会获得相同的实例,前提是缓存尚未失效:
1 2 3 | ResourceBundle resource1 = ResourceBundle.getBundle("test"); ResourceBundle resource2 = ResourceBundle.getBundle("test"); assert resource1==resource2; |
如果你打算节省一些内存,那么你不需要一个单例机制。提供的缓存可以为您提供技巧。
我不是这个主题的专家,但是如果您看看ResourceBundle JavaDocs,也许您可以找到一种更好的方法来处理这个资源包,而不是在这个枚举singlenton中。
"更好"的单例模式不是使用一个。
您描述的方法,像所有通过静态初始化创建单例的方法一样,非常难调试。
相反,使用依赖注入(有或没有框架,如Spring)。
我看到这种方法的问题是代码重复;如果您的singleton有很多方法,那么您最终会编写两次,以确保您的委托逻辑正常工作。查看"初始化按需持有者习语",寻找替代您的方法,它是线程安全的,不需要枚举黑客。
我需要感谢您的讨论,但我需要将私有构造函数代码更新为:
1 2 3 4 |
下面是输出:
1 2 3 4 5 6 | 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值指示反射对象在使用时应抑制Java语言访问检查。false值指示反射对象应该执行Java语言访问检查。
因此,为了测试单线程模式的线程安全性,应该使用多线程应用程序
我喜欢单例的枚举,但是当你需要继承时(就像我在这里做的那样),停止显示。枚举不能继承。在DP4J中,最小的单件看起来是这样的:
1 2 | @com.dp4j.Singleton //(lazy=false) public class MySingleton extends Parent{} |
DP4J将实际创建:
1 2 3 4 5 6 7 8 9 10 11 12 | @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对单例进行单元测试的演示。
Joshua Bloch在他的书《有效Java》中推广了单元的EnUM方法。另一个好方法是懒惰的持有者模式,这类似于OP的想法。我认为将枚举隐藏在opproposed类中不会增加任何性能或并发风险。
尽管单件子通常隐藏在我们使用的框架中,但它们仍然被大量使用。是否使用单件取决于具体情况,我不同意永远不应该使用它们。由于在一些设计不好的系统中过度使用,singleton已经名声不好。