一、饿汉式(线程安全)
1、普通饿汉式单例模式
先介绍第一种饿汉式单例,在类初始化的时候立刻就实例化对象,所以是线程安全的。
/**
* 饿汉式单例模式: 类一加载就会实例化
*/
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
二、懒汉式
1、基于静态内部类的单例模式(线程安全)
第一种懒汉式单例,借助内部类,在外部类调用getInstance()方法时,才会实例化对象。
/**
* 饿汉式内部类单例模式: 类初始化的时候就会进行实例化
*/
public class InnerClassSingleton {
private InnerClassSingleton() {
}
private static class SingletonHolder {
//静态内部类没有生成它的外围类对象的引用
private static InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
//JVM保证了类初始化的时候, 会显式装载SingletonHolder这个内部类, 从而实例化INSTANCE
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
2、锁住整个类的懒汉式单例模式
第二种是最简单的懒汉式单例,将synchronized
关键字加在静态方法上面,直接将整个类都锁住。
/**
* 懒汉式单例: 存在线程安全问题, 这里是直接将这个类给锁住
*/
public class LazySimpleSingleton {
//懒汉式单例模式的时候不能将实例用final修饰, 因为一旦修饰这个变量就不能再指向其他的内存地址
private static LazySimpleSingleton INSTANCE = null;
private LazySimpleSingleton() {
}
//粗粒度加锁, 锁在这个静态方法上相当于把整个类都锁住了(PS: 锁在普通成员方法上只是把当前对象给锁住了)
public static synchronized LazySimpleSingleton getInstance() {
//不使用synchronized可能存在这种情况:
//Thread1进入了if判断, 但是还没实例化, 这时候Thread2也进来了
if (INSTANCE == null) {
INSTANCE = new LazySimpleSingleton();
}
return INSTANCE;
}
}
3、使用双重检查锁的懒汉式单例模式
第三种是细粒度加锁。采用双重同步锁解决懒汉式单例的线程安全问题。
/**
* 懒汉式单例: 存在线程安全问题, 这里用双重同步锁解决线程安全问题
*/
public class LazyDCLSingleton {
//懒汉式单例模式的时候不能将实例用final修饰, 因为一旦修饰这个变量就不能再指向其他的内存地址
private static LazyDCLSingleton INSTANCE = null;
private LazyDCLSingleton() {
}
//DCL(double-checked locking) 双重检测锁保证懒汉式单例模式的线程安全问题
public static LazyDCLSingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazyDCLSingleton.class) {
//如果之前就有多个线程进入了第一重if判断, 而且不判断内层的if的话, 那还是会实例化多次
if (INSTANCE == null) {
INSTANCE = new LazyDCLSingleton();
}
}
}
return INSTANCE;
}
}
看着上面的双重检查锁,可能觉得没问题了,其实还是有问题的。因为JVM会对指令进行重排序,synchronized关键字能保证有序性的前提是锁住的变量整个交给同步块了,但是看上面的代码,INSTANCE并没有完全锁住,具体分析可以看下面的字节码。
正确执行的时候应该如下图:
再直观点可以看下图:
所以只需要改一个地方,private static volatile LazyDCLSingleton INSTANCE = null;
所以当执行INSTANCE的赋值语句时候,会在当前位置加入一个写屏障,该写屏障可以保证写屏障前的指令不会重排序到写屏障之后去。
三、容器式单例
1、Map式单例
Map式单例类似Spring框架。
/**
* 容器式注册单例: Spring
*/
public class ContainerSingleton {
private static ContainerSingleton INSTANCE;
//ConcurrentHashMap只保证往Map中存数和取数是安全的
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className) {
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
Constructor<?> constructor = Class.forName(className).getDeclaredConstructor();
constructor.setAccessible(true);
obj = constructor.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
return ioc.get(className);
}
}
2、Enum枚举型单例
Enum枚举型单例是非常好的单例模式。
/**
* 枚举式单例: JDK从底层为枚举不被序列化、反射破坏单例提供了支持(饿汉式)
*/
public enum EnumSingleton {
//保存到JVM的内存中了
INSTANCE {
//编写枚举的对象方法
protected void print() {
System.out.println("EnumSingleton::print()...");
}
};
protected abstract void print(); //必须声明, 否则无法调用
private Object data; //枚举对象属性
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
//JDK从底层为枚举不被序列化、反射破坏单例提供了支持
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
3、ThreadLocal线程单例
ThreadLocal
实现的单例,每个线程保持一个对象。
/**
* ThreadLocal注册式单例: 每个线程对应一份实例
*/
public class ThreadLocalSingleton {
private ThreadLocalSingleton() {
}
//key是当前线程对象, value是要存储的对象
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
//tomcat很可能就是ThreadLocal搞得, 一个用户一个request相当于开启了一个线程
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}
四、反射攻击单例模式
1、饿汉式单例防御反射攻击
防止反射攻击的方式:
private HungrySingleton() {
if (INSTANCE != null) {
throw new RuntimeException("机制反射调用创建对象");
}
}
验证代码:
//Class clz = Class.forName(InnerClassSingleton.class.getName());
Class clz = HungrySingleton.class;
Constructor c = clz.getDeclaredConstructor();
c.setAccessible(true); //开启权限
HungrySingleton instance = (HungrySingleton) c.newInstance();
HungrySingleton newInstance = HungrySingleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
验证结果:
2、懒汉式单例无法防御反射攻击
① 先调用单例的创建方法,再使用反射的情况。
防止反射攻击的方式:
private LazyDCLSingleton() {
if (INSTANCE != null) {
throw new RuntimeException("禁止反射调用创建对象");
}
}
验证代码:
Class clz = LazyDCLSingleton.class;
Constructor c = clz.getDeclaredConstructor();
c.setAccessible(true);
LazyDCLSingleton instance = LazyDCLSingleton.getInstance();
LazyDCLSingleton newInstance = (LazyDCLSingleton) c.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
验证结果:
② 先使用反射技术,再调用单例的创建方法的情况。
因为如果先通过反射调用来实例化对象,那此时INSTANCE
成员变量还是空的,所以当真正调用单例的创建方法的时候,还是会实例化对象。所以尝试使用静态变量防御反射攻击。
防止反射攻击的方式:
private LazyDCLSingleton() {
if (flag) {
flag = false;
} else {
throw new RuntimeException("禁止反射调用创建对象");
}
}
验证代码(通过反射技术直接修改变量):
Class clz = LazyDCLSingleton.class;
Constructor c = clz.getDeclaredConstructor();
c.setAccessible(true);
LazyDCLSingleton newInstance = (LazyDCLSingleton) c.newInstance();
Field flag = clz.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(newInstance, true);
LazyDCLSingleton instance = LazyDCLSingleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
验证结果:
综上,懒汉式单例无法防御反射破坏单例的攻击,而饿汉式单例可以防御反射攻击!
五、序列化和反序列化破坏单例
1、非枚举单例
除了枚举单例,其他单例实现了序列化接口时候会破坏单例模式!
可增加readResolve
方法来解决单例被破坏的问题。但其实在JVM层面还是实例化了两次对象。
/**
* 序列化与反序列化会破坏单例, 添加readResolve方法可以解决
*/
public class SerializableSingleton implements Serializable {
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {
System.out.println("SerializableSingleton()...");
}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
//增加这个方法后只是覆盖了反序列化后的对象, 之前反序列化出来的对象会被GC回收, JVM层面还是new了两次对象,
private Object readResolve() {
return INSTANCE;
}
}
2、枚举单例
枚举实现的单例模式,不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。还在JDK层面为反射创建对象保驾护航。