1)饿汉式
对内存要求不高的时候可以使用
java
public class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return singleton;
}
}
(2)懒汉式
- 指令重排:
创建一个对象,在JVM中会经过三步:
(1)为singleton分配内存空间
(2)初始化singleton对象
(3)将singleton指向分配好的内存空间
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。
java
public class Singleton {
//volatile防止指令重排并使其变更在不同线程中可见
private static volatile Singleton singleton;
//构造函数私有化,防止外界构造对象
private Singleton(){}
//静态方法,可以直接用类.方法调用
public static Singleton getInstance() {
//双检法,避免先要获取锁才能进行检查,提高并发性能
if (singleton == null) {
synchronized(Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
(3)单例模式的破坏
饿汉式和懒汉式都可以破坏
反射
可以通过反射强制访问被私有的构造器,去创建对象
java
public static void main(String[] args) {
// 获取类的显式构造器
Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
序列化 和反序列化
当你将一个单例对象写入文件(或任何其他输出流),然后从该文件读取时,Java 的序列化机制会创建一个新的对象实例。
为了防止这种行为破坏单例模式,你需要重写 `readResolve()` 方法,在 `readResolve()` 方法中返回单例实例的引用,这样即使经过序列化和反序列化,你仍然只会有一个单例实例。
java
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
(4)枚举
在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化
好处:简洁,防止被反射等破坏
java
public enum Singleton {
INSTANCE;
Singleton() { System.out.println("枚举创建对象了"); }
public static void main(String[] args) { /* test(); */ }
public void test() {
Singleton t1 = Singleton.INSTANCE;
Singleton t2 = Singleton.INSTANCE;
System.out.print("t1和t2的地址是否相同:" + t1 == t2);
}
}
// 枚举创建对象了
// t1和t2的地址是否相同:true
枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名 输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
参考: 三种单例模式写法及解析