设计模式之单例模式
单例模式
保证一个类只有一个实例,并提供一个访问它的全局访问点。单例模式是创建型模式。单例模式在显示生活中应用的非常广泛。很多职位都是只能有一个,比如:国家主席、公司CEO,在开发中使用的数据库连接池等等。
前言
谈到单例模式,相信很多小伙伴会感觉So easy!!!,这么简单的东西还有必要拿出来说?我一开始也是这么认为的,直到有一次面试,面试官让我写一个单例模式,当时心里还偷笑,啥玩意?我没听错吧,考我这么简单的问题,接着我就写出了下面的代码
public class HungrySingleton { private static HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return hungrySingleton; } }
面试官看了看,貌似不太满意的样子,于是和我说,写一个懒汉式的单例,于是就有了下面的代码
public class LazySingleton { private static LazySingleton singleton = null; private LazySingleton() { } public static LazySingleton getInstance() { if (singleton == null) { singleton = new LazySingleton(); } return singleton; } }
面试官又问,这样能保证单例?突然想起来多线程访问的时候可能创建多个实例,于是在访问方法上加了synchronized关键字,心想,这下没问题了吧,面试官又问,确定这样可以保证单例? 额...... 回去后仔细查了一下单例模式,发现自己面试时写的饿汉式单例有3个问题,懒汉式在加了synchronized后同样存在3个问题,而且加锁之后性能会受影响。下面就来依次说明这些问题
如何保证单例及安全性
问题1:上面写的单例类没有加final关键字,那么也就可以被继承,继承后创建子类对象,所以无法保证单例。在这里,如果没有真正写代码验证或是基础不太好的朋友可能认为这个说法没问题,真的是吗?这里只讨论外部类继承单例类的情况,子类作为单例类的内部类的情况不讨论,看一下测试代码
报错已经提示的很明显了,如果一个类只有私有的构造方法,那么这个类将无法被被继承,因为子类必须调用父类的构造方法进行初始化,但却无法访问到父类的构造方法(因为是私有的)。所以,以上的单例类即便没有声明为final,也是不可能继承的,因为编译不通过!不过个人感觉还是加上final关键字,这样就很清晰的说明该类不可继承。
问题2:反射方式破坏单例
public static void main(String[] args) { try{ //很无聊的情况下, 进行破坏 Class<?> clazz = HungrySingleton.class; //通过反射拿到私有的构造方法 Constructor c = clazz.getDeclaredConstructor(null); //强制访问 c.setAccessible(true); //暴力初始化 HungrySingleton o1 = (HungrySingleton)c.newInstance(); HungrySingleton singleton = HungrySingleton.getInstance(); System.out.println("singleton = " + singleton); System.out.println("o1 = " + o1); System.out.println(o1 == singleton); }catch (Exception e){ e.printStackTrace(); } }
运行结果:
很明显,单例被破坏了,怎么解决呢?既然是调用构造方法进行创建对象的,那么就可以修改构造方法,在里面加入抛出异常的代码,这样,当反射调用时,自然就会报错,这样,就把通过反射创建对象的路堵死了。
private HungrySingleton() { if(hungrySingleton != null) { throw new RuntimeException("不允许创建多个实例!"); } }
再次运行:
至此,通过反射破坏单例的问题得到解决。
问题3:反射修改单例对象
public static void main(String[] args) throws Exception { HungrySingleton singleton = HungrySingleton.getInstance(); System.out.println("singleton=" + singleton); Field[] declaredFields = HungrySingleton.class.getDeclaredFields(); for (Field field : declaredFields) { field.setAccessible(true); field.set(null, null); } System.out.println("HungrySingleton.getInstance()=" + HungrySingleton.getInstance()); }
运行结果:
是不是有偷梁换柱的感觉,再调用getInstance()就坑了,空指针异常应该会搞得比较懵,好好的单例就这样被破坏了。解决方案也很简单,hungrySingleton加上final修饰符。再次运行:
问题4:序列化方式破坏单例
因为序列化必须实现Serializable接口,下面让HungrySingleton实现Serializable接口
public class HungrySingleton implements Serializable { private static final HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton() { if(hungrySingleton != null) { throw new RuntimeException("不允许创建多个实例!"); } } public static HungrySingleton getInstance() { return hungrySingleton; } }
测试代码:
public static void main(String[] args) throws Exception { HungrySingleton singleton1 = HungrySingleton.getInstance(); HungrySingleton singleton2 = null; //序列化 OutputStream os = new FileOutputStream("singleton1.obj"); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(singleton1); os.close(); oos.close(); //反序列化 InputStream is = new FileInputStream("singleton1.obj"); ObjectInputStream ois = new ObjectInputStream(is); singleton2 = (HungrySingleton) ois.readObject(); ois.close(); is.close(); System.out.println(singleton1); System.out.println(singleton2); //对比singleton1与singleton2是否为同一个对象 System.out.println(singleton1 == singleton2); }
运行结果:
结果证明每反序列化一次,就会产生一个新的对象,违背了单例模式的初衷。那要怎样保证序列化的情况下也能够实现单例呢,也很简单,只需要加入readResolve() 方法即可,来看修改后的代码
public final class HungrySingleton implements Serializable { private static final HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton() { if(hungrySingleton != null) { throw new RuntimeException("不允许创建多个实例!"); } } public static HungrySingleton getInstance() { return hungrySingleton; } public Object readResolve() { return hungrySingleton; } }
再看运行结果:
为什么加了readResolve() 方法就保证单例了呢?下面就跟进源码一探究竟
进入ois.readObject()方法内部
readObject()方法内部调用了readObject0()方法
readObject0()内部调用了readOrdinaryObject()
到这里还没有找到为什么加上readResolve()方法就避免了单例被破坏的原因,继续往下
hasReadResolveMethod()方法通过readResolveMethod != null判断要反序列化的对象中是否有readResolve()方法
搜索一下readResolveMethod在哪里被赋值
再回到readOrdinaryObject()方法中,调用invokeReadResolve()方法,方法内部调用readResolve()方法
至此,我们通过添加readResolve()方法的方式解决了单例被破坏的问题,不过通过跟源码我们可以看到在反射调用readResolve()方法之前,先用反射创建了类的实例对象obj(前面说过通过反射创建对象的问题已经解决了,为什么这里面还能创建成功呢?那是因为反序列化方式创建对象反射调用的构造方法不是我们在单例类里面写的构造方法,所以在单例类中写的抛出异常的代码在反序列化创建对象的时候不可能执行!),之后这个obj被覆盖,这样的话,每次调用都会创建一个多余的对象,只是这个对象没有返回而已,如果创建对象的频率增大,意味着内存开销的增大,有没有办法从根本上解决问题呢?答案是一定的,那就是使用注册式单例,这个后面再说。我们先来看一下经过前面的改造,比较完美的单例代码要怎么写
饿汉式
/** * 饿汉式单例 */ public final class HungrySingleton implements Serializable { //使用final关键字防止子类继承 private static final HungrySingleton hungrySingleton = new HungrySingleton(); //类加载时初始化,final关键字防止通过反射修改 private HungrySingleton() { if(hungrySingleton != null) { throw new RuntimeException("单例类不允许反射创建对象!"); } } public static HungrySingleton getInstance() { return hungrySingleton; } public Object readResolve() { return hungrySingleton; //解决反序列化创建对象的问题 } }
上面我们一直在说饿汉式单例,现在来看一下懒汉式单例,如果我们想当单例对象真正被使用时,也就是getInstance()方法被调用时再初始化单例,要怎么实现呢?下面来看常用的2中实现
懒汉式
写法1,双重判断方式,这种方式有一个缺点,singleton字段没有声明为final的,可以通过反射修改其值。
/** * 双重判断懒加载 */ public final class LazyDoubleCheckSingleton implements Serializable { private volatile static LazyDoubleCheckSingleton singleton = null; private LazyDoubleCheckSingleton() { if (singleton != null) { throw new RuntimeException("不允许创建多个实例!"); } } public static LazyDoubleCheckSingleton getInstance() { if (singleton == null) {//1.并发时可能出现多个线程同时进入此判断 synchronized(LazyDoubleCheckSingleton.class) { //控制所粒度,只有在单例未初始化时才会使用到锁,之后的的访问不会使用到锁,提升性能 //2.因为加了锁,所以某一时刻只能有一个线程进入同步代码块,当前面的线程创建单例并释放锁后, // 后面的一个线程获得锁,如果不再次判断是否为null,则前面创建的对象会被后来创建的对象覆盖 if (singleton == null) { singleton = new LazyDoubleCheckSingleton(); } } } return singleton; } public Object readResolve() { return getInstance(); } }
写法2,静态内部类方式(推荐)
/** * 静态内部类方式懒加载 */ public final class LazyInnerClassSingleton implements Serializable { private LazyInnerClassSingleton() { if (LazyHolder.LAZY != null) { throw new RuntimeException("不允许创建多个实例!"); } } public LazyInnerClassSingleton getInstance() { return LazyHolder.LAZY; } //默认不加载,只有在new对象或者LazyHolder内部静态成员或静态方法第一次被调用时加载 //多线程并发的情况下,需要等待类加载完成,加载完成后,线程继续执行,此时LAZY对象已创建完成 //所以不会出现创建多个对象的情况 private static class LazyHolder{ private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } public Object readResolve() { return LazyHolder.LAZY; } }
注册式单例(解决频繁序列化创建对象内存消耗问题)
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例,注册式单例有2种写法:枚举登记和容器缓存。先来看一下枚举单例的写法
注册式单例-枚举登记
public enum EnumSingleton { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; } }
来看一下测试代码
@Test public void enumSingletonTest() throws Exception{ EnumSingleton singleton1 = EnumSingleton.getInstance(); singleton1.setData(new Object()); EnumSingleton singleton2 = null; OutputStream os = new FileOutputStream("enumSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(singleton1); os.close(); oos.close(); InputStream is = new FileInputStream("enumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(is); singleton2 = (EnumSingleton) ois.readObject(); is.close(); ois.close(); System.out.println(singleton1.getData()); System.out.println(singleton2.getData()); System.out.println(singleton1.getData() == singleton2.getData()); System.out.println(singleton1 == singleton2); }
运行结果:
没有做任何处理,可以看到反序列化的对象和我们序列化的对象是同一个对象,那么,枚举式单例是怎么做到这点的呢?下面我们使用java反编译工具Jad反编译EnumSingleton类的class文件,使用jad class路径 ,看一下反编译后的代码
可以看到枚举式单例在静态代码块中对INSTANCE进行了赋值,是饿汉式的体现。
下面再来看一下为什么序列化破坏不了枚举式单例,还是ois.readObject() -> readObject0(false) 不同的是这次不是case语句这次是执行TC_ENUM,见下图
readEnum(unshared)
从上面的代码可以看出,枚举式单例在反序列化时,并没有反射创建对象,而是通过字节码对象Class和枚举名字从内存中找到对应的枚举常量。
下面再来看一下反射会不会破坏枚举式单例:
@Test public void enumSingletonTest2() throws Exception{ Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(); constructor.newInstance(); }
运行结果:
报的是 java.lang.NoSuchMethodException 异常 ,意思是没有找到无参的构造方法,我们打开jad反编译后的枚举类代码,发现只有一个private的无参构造,见下图
修改代码,再次反射
public void enumSingletonTest2() throws Exception{ Class<EnumSingleton> clazz = EnumSingleton.class; Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); constructor.newInstance("test",1); }
运行结果:
提示不能用反射创建枚举对象。查看源码后得到下面的代码
jdk中在创建对象的时候做了判断,如果是枚举类型,则直接抛出异常!从根本上保证了枚举的单例性,让枚举式单例成为一种比较优雅的写法。枚举式单例也是《Effective Java》书中推荐的一种单例实现写法 。
注册式单例-容器缓存
public class ContainerSingleton { private ContainerSingleton() { } private static final Map<String, Object> ioc = new ConcurrentHashMap<>(); public static Object getBean(String className) { if (ioc.containsKey(className)) { return ioc.get(className); } else { Object obj = null; try { obj = Class.forName(className).newInstance(); ioc.put(className, obj); } catch (Exception e) { e.printStackTrace(); } return obj; } } }
容器式单例适用于创建的实例非常多的情况,便于管理。