现在更好的Java单例模式?

时间:2011-04-28 17:44:04

标签: java design-patterns enums singleton

您知道,自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”模式的所有优点吗?这似乎从两个世界都变得更好。你怎么看?有没有更好的方法来实现这一目标? 谢谢。

修改
@all
首先,在Effective Java,第2版中提到了Singleton模式的枚举用法:wikipedia:Java Enum 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

应该提到什么:

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

    因此,总结一下: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();
        }
    }
    

    注册表类是(注意,注册表和数据类显式应该实现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
    

    应该提到什么:

    1. Singleton实例创建了几个!次
    2. 所有注册表对象都是具有自己数据的不同对象
    3. 因此,总结一下:标准方法对于反射攻击是弱的,并且在序列化后返回不同的实例,但是对于相同的数据则是。


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

7 个答案:

答案 0 :(得分:16)

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

答案 1 :(得分:3)

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

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

相反,使用依赖注入(使用或不使用Spring等框架)。

答案 2 :(得分:3)

  

[...]推荐的写作方式   Java中的单例模式正在使用   枚举[...]

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

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

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

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

您希望实现的目标是确保ResourceBundle的唯一实例,不确定它的两个实例是否会以任何可能的方式影响您的应用程序,但无论如何,ResourceBundle是由它实现的JDK已经缓存了资源包实例。

Javadocs说:

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

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

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

如果您打算保存一些内存,那么您不需要单例机制。提供的缓存可以帮到你。

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

答案 3 :(得分:2)

我用这种方法看到的问题是代码重复;如果你的单身人士有很多方法,你最终会写两次以确保你的委托逻辑有效。查看“initialization on demand holder idiom”以获取您的方法的替代方法,该方法是线程安全的,不需要枚举。

答案 4 :(得分:1)

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

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. 所有注册表对象都是具有自己数据的不同对象
  3. 因为我们强制私有构造函数公开 c.setAccessible(真);

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

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

答案 5 :(得分:0)

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

Singletons仍然被大量使用,尽管它们通常隐藏在我们使用的框架中。是否使用Singleton取决于具体情况,我不同意永远不会使用它们。由于在一些设计不良的系统中过度使用,Singleton的名字很糟糕。

答案 6 :(得分:0)

我喜欢Singleton的枚举,但是show stop就是你需要继承的时候(就像我here)。枚举不能继承。 使用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;
  }

正如您所指出的,此解决方案容易受到“反射攻击”的影响。在dp4j.com上,确实演示了如何使用Reflection API对Singleton进行单元测试。