单例模式
定义:保证一个类只有一个实例,并且提供一个全局访问点
场景:线程池,数据库连接池等
实现方式:
1.懒汉模式:
只有使用的时候,才初始化。延迟加载
//使用的时候才初始化
//jvm 一个实例
public class LazySingleton {
private volatile static LazySingleton instance; //volatile防止CPU指令重排
private LazySingleton(){}
public static LazySingleton getInstance() {
if(null==instance){ //双重检查
synchronized(LazySingleton.class){
if(null==instance)
instance=new LazySingleton();
}
}
return instance;
}
}
给变量加volatile是为了防止CPU指令重排,我们来看下方的Demo例子:
public class Demo {
public static void main(String[] args) {
Demo demo=new Demo();
}
}
0: new #1 // class Demo 1.分配空间,返回一个指向该空间的内存引用
3: dup
4: invokespecial #16 // Method "<init>":()V 2.对空间进行初始化
7: astore_1 // 3.把内存引用赋值给Demo变量
“4:”和“7:”这两个指令顺序可能会颠倒(reordering),也就是先把内存引用赋值给Demo变量,然后对空间进行初始化。这时候就出现问题了,我们看LazySingleton这个类:如果内存引用赋值给instance变量后,还没有进行初始化,这时候另外一个线程进入"getInstance()"方法,他会发现这个instance已经被赋值了,就会直接执行“return instance”来返回实例,但是这个实例还没有被初始化,这就可能会导致空指针等异常。所以,我们要在变量前加volatile修饰来防止CPU重排。
总结:
(1).保证线程安全
(2).防止指令重排
(3).双重检查,优化加锁过程
2.饿汉模式:
在类加载阶段就完成了实例的初始化,通过类加载机制保证线程安全
类加载过程:
(1).类加载:加载对应的二进制文件,在方法区创建对应的数据结构 (加载class文件到方法区,在方法区创建相应类对象)
(2).连接:a.验证(确保Class文件的正确性) b.准备(为静态变量分配内存和设置初始值) c.解析(符号引用替换为直接引用)
(3).初始化:给静态属性赋值
public class HungrySingleton {
private static HungrySingleton instance=new HungrySingleton();
public static HungrySingleton getInstance(){
return instance;
}
private HungrySingleton() {}
}
3.静态内部类:
public class InnerClassSingleton {
static class InnerClass{
private static InnerClassSingleton instance=new InnerClassSingleton();
}
public static InnerClassSingleton getInstance(){
return InnerClass.instance;
}
private InnerClassSingleton() {
if(InnerClass.instance!=null){
throw new RuntimeException("单例类不允许多个实例(不允许反射来破坏单例模式)");
}
}
}
在外界调用"getInstance()" 的时候,内部类InnerClass才会被类加载器加载,然后在加载过程中初始化instance。为了防止遭到反射攻击,可以在私有构造器中加入初始化判断,如果instance已被初始化,就说明单例类遭到破坏,此时可以向外界抛出异常
4.枚举类型:
public enum EnumSingleton {
INSTANCE;
}
反射机制不允许实例化枚举类型,源码如下:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
枚举类型也可以防止反序列化攻击,源码如下:
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
当使用反序列化获取枚举对象时,会通过"valueOf()"方法来根据名字(name)查找枚举对象。
5.序列化:
public class SerializableSingleton implements Serializable{
static final long serialVersionUID=42L;
private static SerializableSingleton instance=new SerializableSingleton();
public static SerializableSingleton getInstance(){
return instance;
}
private SerializableSingleton() {
}
//反序列化时,从此方法中拿取实例
Object readResolve() throws ObjectStreamException{
return instance;
}
}
serialVersionUID:如果我们没有设置这条属性,系统会默认的帮我们随机分配一个UID,这时候我们将这个类序列化到磁盘上。然后当我们修改这个类中的内容的时候,系统又会随机再分配一个UID。此时修改后的类和磁盘上的类因为UID不同,所以互相不兼容。即:SerializableSingleton instance != (SerializableSingleton)object
readResolve():在反序列化时获得的对象和通过"getInstance()"方法获得的对象不是同一个对象,为了解决这个问题,我们写一个特殊方法:"readResolve()",有了这个方法,在反序列化过程中,就会从这个方法中获得该类的实例。
我们用ObjectOutputStream和ObjectInputStream来写出和读入:
private static void useSerializableSingleton() throws Exception {
SerializableSingleton instance=SerializableSingleton.getInstance();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("singleton"));
objectOutputStream.writeObject(instance);
objectOutputStream.close();
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("singleton"));
Object object=objectInputStream.readObject();
objectInputStream.close();
SerializableSingleton instance1=(SerializableSingleton)object;
System.out.println(instance1);
}