在创建单例类时,请考虑使用枚举。它解决了可能会因反序列化和反射而出现的问题。
单例是应该在每个JVM中只有一个实例的类。单例类的Same实例被多个线程重用。大多数情况下,我们使用单例来表示系统配置和窗口管理器,因为这些实例应该是JVM中所有线程和对象所共有的。【优锐课】抽丝剥茧 细说架构那些事抽丝剥茧 细说架构那些事
制作单例的传统方法
有几种流行的制作单例的方法。
方法1:具有公共静态最终字段的单例
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {}
}
方法2:使用公共静态工厂方法的单例
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance(){
return INSTANCE;
}
}
方法3:具有延迟初始化的单例
public class Singleton {
private static Singleton INSTANCE = null;
private Singleton() {}
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
上述方法的优缺点
以上所有方法都使用私有构造函数强制执行不满足要求(无法创建实例)。在这里,即使我们没有任何事要做,我们也无法避免创建私有构造函数。因为如果这样做,那么将使用与该类相同的访问修饰符创建一个隐式的无参数默认构造函数。例如,如果将该类声明为public,则默认构造函数为public否则,默认构造函数为public。如果该类被声明为protected,则默认构造函数为受保护的(有关更多详细信息,请参考Oracle文档)。
比较上述方法,前两种方法根本没有性能差异。方法1更清晰,更简单。方法2的一个小优点是,以后,你可以使该类成为非单一类,而无需更改API。你可以通过更改factory方法的实现为每个调用创建一个新实例,而不是按以下方式返回相同的实例来实现。
public static Singleton getInstance() {
return new Singleton ();
}
静态字段在类加载时初始化。因此,在方法1和2中,即使在我们在运行时不使用它们的情况下,也会创建单例实例。只要单例对象不是太大,并且创建实例也不是太昂贵,就没有问题。方法3避免了延迟初始化的问题。在方法3中,实例是在我们第一次访问单例对象时创建的。细粒度同步用于确保使用多个并发线程创建的对象不超过一个。
在不使用单例类进行序列化和反序列化之前,上述所有方法都可以正常工作。让我们再想想:我们如何通过上述方法实现单例行为?通过将构造函数设为私有,并使构造函数不可用于创建类的新实例来实现。但是除了构造函数之外,没有其他方法可以创建类的实例吗?答案是不。还有其他一些高级方法。
1. 序列化和反序列化
2. 反思
序列化和反序列化的问题
为了序列化上述单例类,我们必须使用Serializable接口实现这些类。但是,这样做还不够。反序列化类时,将创建新实例。现在,构造函数是否私有都无关紧要。现在,JVM中可能有多个同一个单例类的实例,这违反了单例属性。
public class SerializeDemo implements Serializable {
public static void main(String[] args) {
Singleton singleton = Singleton.INSTANCE;
singleton.setValue(1);
// Serialize
try {
FileOutputStream fileOut = new FileOutputStream("out.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(singleton);
out.close();
fileOut.close();
} catch (IOException e) {
e.printStackTrace();
}
singleton.setValue(2);
// Deserialize
Singleton singleton2 = null;
try {
FileInputStream fileIn = new FileInputStream("out.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
singleton2 = (Singleton) in.readObject();
in.close();
fileIn.close();
} catch (IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException c) {
System.out.println("singletons.SingletonEnum class not found");
c.printStackTrace();
}
if (singleton == singleton2) {
System.out.println("Two objects are same");
} else {
System.out.println("Two objects are not same");
}
System.out.println(singleton.getValue());
System.out.println(singleton2.getValue());
}
}
上面代码的输出是:
Two objects are not same
2
1
在这里,singleton和singleton2是两个不同的实例,具有两个不同的值作为其字段变量。这违反了单例属性。解决方案是我们必须实现readResolve方法,该方法在准备反序列化的对象并将其返回给调用者之前准备好。解决方法如下。
public class Singleton implements Serializable{
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
protected Object readResolve() {
return INSTANCE;
}
}
现在,以上代码的输出将是:
Two objects are same
2
2
现在,保留了singleton属性,并且JVM中仅存在singleton类的一个实例。
反思问题
高级用户可以在运行时使用反射将构造函数的私有访问修饰符更改为所需的任何内容。如果发生这种情况,我们唯一的非不稳定机制就会中断。让我们看看如何做到这一点。
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.INSTANCE;
Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
Singleton singleton2 = (Singleton) constructor.newInstance();
if (singleton == singleton2) {
System.out.println("Two objects are same");
} else {
System.out.println("Two objects are not same");
}
singleton.setValue(1);
singleton2.setValue(2);
System.out.println(singleton.getValue());
System.out.println(singleton2.getValue());
}
}
输出:
Two objects are not same
1
2
这样,不可访问的私有构造函数变得可访问,并且使类成为单例的整个想法被打破。
在Java中使用枚举创建单例
通过使用枚举类型使单例很容易解决所有上述问题。
枚举单例
public enum Singleton {
INSTANCE;
}
上面的三行代码构成了一个单例,没有讨论任何问题。由于枚举本质上是可序列化的,因此我们不需要使用可序列化的接口来实现它。反射问题也不存在。因此,可以100%保证JVM中仅存在一个单例实例。因此,建议将此方法作为Java中制作单例的最佳方法。
如何使用
public enum SingletonEnum {
INSTANCE;
int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
主类实现:
public class EnumDemo {
public static void main(String[] args) {
SingletonEnum singleton = SingletonEnum.INSTANCE;
System.out.println(singleton.getValue());
singleton.setValue(2);
System.out.println(singleton.getValue());
}
}
这里要记住的一件事是,当序列化一个枚举时,字段变量不会被序列化。例如,如果我们对SingletonEnum
类进行序列化和反序列化,则将丢失int值字段的值(有关枚举序列化的更多详细信息,请参考Oracle文档)。