现在更好的 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 处理enums 的方式使它们成为单例模式的理想候选者。另外,如果您最终因单例而面临并发/数据共享问题,您总是可以让getInstance() 返回一个新实例,而这在enums 中是不可能的。当然,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 单例模式? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

单例模式中为什么用枚举更好?

Java中的单例模式和静态类有啥区别? [复制]

单例模式之原型模式

线程安全在Java中实现单例模式的有效方法? [复制]

Java单例模式深入详解

Java设计模式--单例模式