现在更好的Java单例模式?

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
private ApplicationSingleton() {
    long currentTime = System.currentTimeMillis();
    System.out.println("Singleton instance is created:" + currentTime);
}

下面是输出:

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已经名声不好。