设计模式学习笔记,感谢geely老师的《Java设计模式精讲 Debug方式+内存分析》课程。
单例模式(Singleton Pattern)
定义:
保证一个类仅有一个实例,并提供一个全局访问点
类型:
创建型
适用场景:
想确保任何情况下都绝对只有一个实例
ServletContext单实例多线程,ServletConfig,ApplicationContext,DBPool
优点:
在内存里只有一个实例,减少了内存开销;可避免对资源的多重占用;设置全局访问点,严格控制访问
缺点:
没有接口,扩展困难
重点:
- 私有构造器
- 线程安全
- 延迟加载
- 序列化和反序列化安全
- 反射
实现:
懒汉式:
指令重排序问题
1、禁止重排序
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
instance = new LazyDoubleCheckSingleton();
}
return instance;
}
}
2、基于类初始化的延迟加载
public class StaticInnerClassSingleton {
private static class InnerClass {
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.instance;
}
}
public StaticInnerClassSingleton() {
}
饿汉式:
public class HungrySingleton {
private final static HungrySingleton instance;
static {
instance = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
序列化破坏单例模式
因为实现了序列化接口,isInstantiable()
返回true,所以在序列化时会反射创建新的实例,破坏了单例模式。
解决:
需要添加一个readResolve()
方法
private Object readResolve() {
return instance;
}
如果含有readResolve
方法,则会通过反射调用该方法。
因此,根据添加的方法,返回的是已经创建过的同一实例
反射攻击
虽然空参构造函是private的,但可以设置构造器的权限,就可以反射调用构造器创建实例,于是就会产生不同的实例。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class hungrySingletonClass = HungrySingleton.class;
Constructor declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
HungrySingleton instanceByCons = (HungrySingleton) declaredConstructor.newInstance();
HungrySingleton instance = HungrySingleton.getInstance();
System.out.println(instanceByCons == instance);
}
解决:
饿汉式与静态内部类的懒汉式可使用该方案,因为在类加载时就实例化了对象,因此instance不为null,反射调用构造器时就会抛出异常
private HungrySingleton() {
if (instance != null) {
throw new RuntimeException("单例构造器静止反射调用");
}
}
而不是在类加载就创建实例的懒汉式的不能防御反射攻击
但是有一种单例既可以防止序列化破坏,又可以防御反射攻击,那就是枚举单例
枚举单例
public enum EnumInstance {
INSTANCE;
@Getter
@Setter
private Object data;
public static EnumInstance getInstance() {
return INSTANCE;
}
}
序列化
测试返回true
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(instance);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("singleton")));
EnumInstance enumInstance = (EnumInstance) ois.readObject();
System.out.println(instance.getData() == enumInstance.getData());
}
ObjectInputStream中
的readEnum()
方法
readString()
获取到枚举对象的名称name
通过Class类型和name获取枚举常量,这个枚举常量是唯一的,就能保证单例
反射
public static void main(String[] args) throws Exception {
Class instanceClass = EnumInstance.class;
Constructor constructor = instanceClass.getDeclaredConstructor();
constructor.setAccessible(true);
}
报错NoSuchMethodException
,是getDeclaredConstructor()
那一行
获取构造器时没有获得无参构造器
查看Enum类,发现它只有一个有参的构造方法
那我们根据有参构造进行获取构造器
public static void main(String[] args) throws Exception {
Class instanceClass = EnumInstance.class;
Constructor constructor = instanceClass.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumInstance instance = (EnumInstance) constructor.newInstance("Orcas", 11);
}
这个时候报错的就是创建实例那一行
不能反射创建枚举对象
我们可以在Constructor
类中看到,在构建实例时,它会进行判断是否是枚举类,如果是,它就会抛出刚才的异常
由此可见,枚举类可以通过ObjectInputStream和Constructor类中相应的逻辑来避免序列化和反射对单例的破坏。
而枚举类本身我们通过反编译可以看到
它拥有私有构造器,在加载时初始化完成了INSTANCE,类似饿汉模式
// 枚举类被final修饰
public final class EnumInstance extends Enum {
...
...
// 私有构造器
private EnumInstance(String s, int i)
{
super(s, i);
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
// 静态
public static final EnumInstance INSTANCE;
private Object data;
private static final EnumInstance $VALUES[];
// 通过静态代码块来实例化INSTANCE
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
容器单例
线程不安全
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> singletonMap = new HashMap<>();
public static void putInstance(String key, Object instance) {
if (StringUtils.isNotBlank(key) && instance != null) {
if (!singletonMap.containsKey(key)) {
singletonMap.put(key, instance);
}
}
}
public static Object getInstance(String key) {
return singletonMap.get(key);
}
}
ThreadLocal线程单例
同一个线程中单例
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> threadLocalInstance = new ThreadLocal<ThreadLocalInstance>() {
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
private ThreadLocalInstance() {
}
public static ThreadLocalInstance getInstance() {
return threadLocalInstance.get();
}
}
单例模式的应用
1、Runtime
饿汉式
2、Desktop
类似容器单例
3、AbstractFactoryBean
4、Mybatis中ErrorContext
基于ThreadLocal的线程单例