单例模式
单例模式是指在内存中只会创建一次对象的设计模式。在程序中若可以多次使用同一对象且作用相同时则一般选择单例模式,可以避免创建过多相同功能的对象导致内存飙升,达到对象复用,减少内存消耗的效果。
单例模式有两种类型
1.懒汉式:当需要使用对象时再创建。
2.饿汉式:在类加载阶段就进行对象创建。
懒汉式:
对于懒汉式单例,最经典的即是双重检测(double check)。
public class Singleton {
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;
}
}
对于双重检测写法,值得注意的就是private static volatile Singleton singleton;
中所加的volatile关键字。
volatile关键字在此处起防止指令重排序作用。
我们知道,当对象初始化时须经历三个阶段,分别是
1.分配内存空间
2.初始化对象
3.将引用指向所分配的内存地址
在对象初始化阶段有可能会发生指令重排序,此时2和3执行顺序可能会出现颠倒的情况,即先将引用指向内存空间(此时引用不为空)再初始化对象。此时就有可能会出现线程拿到未初始化完成的单例对象。
举个例子:
A线程经过双重检测之后发现对象引用为空,则开始创建对象。此时,jvm对该对象创建过程进行了指令重排序,执行顺序变为
1.分配内存空间
3.将引用指向所分配的内存地址(此时引用不为空)
2.初始化对象
当A线程执行到第二步:“将引用指向所分配的内存地址”时,singleton
引用便不再为空了,此时A线程却仍未对对象进行初始化,即singleton
引用指向的是一个未初始化完成的对象。此时B线程进行判空操作,发现singleton
引用并不为空,则返回了A线程未初始化完成的对象。
饿汉式:
public class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return singleton;
}
}
破坏单例:
利用反射和序列化反序列化操作即可破坏以上两种单例模式。
反射:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取构造器
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
//访问私有构造方法
constructor.setAccessible(true);
Singleton singleton = constructor.newInstance();
System.out.println(singleton == Singleton.getInstance());//返回false
}
序列化反序列化:
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("d://Singleton"));
objectOutputStream.writeObject(Singleton.getInstance());
File file = new File("d://Singleton");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Singleton newinstance = (Singleton) objectInputStream.readObject();
System.out.println(Singleton.getInstance() == newinstance);
}
两个对象地址不相等的原因是:readObject() 方法读入对象时它必定会返回一个新的对象实例,必然指向新的内存地址。
避免被破坏:
《Effective Java》一书中提到一个解决方法,可以避免被破坏。
public enum Singleton {
INSTANCE;
}
即是使用枚举类型,枚举类型具有天然的线程安全性和单一实例性质,当枚举类被加载时便会调用其空参构造器构造INSTANCE对象,之后便不会再创建(属于饿汉式)。枚举类型最亮眼的是防止反射和序列化反序列化操作破坏单例模式。
1.反射:
在反射构造对象过程中会判断该类是否为枚举类型,若是,则抛出异常,终止创建。
2.序列化反序列化
枚举类型在进行序列化时只是写入枚举类类型和名字,反序列化时进行读出并通过名字使用valueOf方法获取对应实例。
私货:
无