1.3.1.设计模式03-单例模式【上】
时长:1h33min
课程目标:
》单例模式的应用场景
》IDEA下的多线程调试方式
》保证线程安全的单例模式策略
》掌握反射暴力攻击单例解决方案及原理分析
》序列化破坏单例的原理及解决方案
》掌握常见的单例模式写法
》原型模式的应用场景及常用写法
学习目的:
》深入掌握单例模式
》解决面试题
1.单例模式的定义
单例模式,Singleton Pattern.
定义:
是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
隐藏所有的构造方法【私有】
创建创建型模式。
1.1.适用场景
确保在任何情况下都只有一个实例。
生活中单例体现:
国家主席,公司ceo,部门经理。
代码中单例类:
ServletContext,ServletConfig,ApplicationContext,DBPool
为什么它是单例的呢?
如:配置文件,只能有一个,否则该使用谁呢。【唯一性主权】
2.单例模式常见用法
1.饿汉式单例
2.懒汉式单例
3.注册式单例
4.ThreadLocal单例
2.1.饿汉式单例
人很饿------》着急吃饭---》在类首次加载时创建实例【用不用,先吃饱再说】
2.1.1.代码示例
package com.wf.singleton; /** * @ClassName HungrySingleton * @Description 饿汉式单例 * @Author wf * @Date 2020/4/29 17:28 * @Version 1.0 */ public class HungrySingleton { //首次加载静态成员 public static final HungrySingleton singleton = new HungrySingleton(); //构造私有 private HungrySingleton(){} //全局访问点 public static HungrySingleton getInstance(){ return singleton; } }
第二种写法:
package com.wf.singleton; /** * @ClassName HungryStaticSingleton * @Description 饿汉式单例---静态块初始化 * @Author wf * @Date 2020/4/29 17:31 * @Version 1.0 */ public class HungryStaticSingleton { //final保证反射不会改变实例 private static final HungryStaticSingleton instance; private HungryStaticSingleton(){} static{ instance = new HungryStaticSingleton(); } public static HungryStaticSingleton getInstance(){ return instance; } }
2.1.2.优缺点总结
优点:
执行效率高,性能高【无锁】
缺点:
可能生成内存浪费,因为实例不被使用,也进行new .
说明:
这种写法,只能在小范围内使用。
2.2.懒汉式单例
人很懒------不着急-----等到使用时再创建
2.2.1.示例代码
package com.wf.singleton; /** * @ClassName LazySimpleSingleton * @Description 懒汉式单例 * @Author wf * @Date 2020/4/29 17:43 * @Version 1.0 */ public class LazySimpleSingleton { private static LazySimpleSingleton instance; private LazySimpleSingleton(){} public static LazySimpleSingleton getInstance(){ if(null == instance) { instance = new LazySimpleSingleton(); } return instance; } }
2.2.2.总结
优点:
节省内存空间,使用时再创建实例。
缺点:
线程不安全。性能差。
为什么会线程不安全呢?
线程不安全来自于getInstance方法,方法的调用者最终归结到线程。
当出现多个线程,同时调用getInstance方法时,线程间会抢占资源。如下所示:
这是理论上的分析。下面在idea中进行代码调试,说明这种线程不安全性。
A.多线程调试线程安全性
【1】创建测试类
package com.wf.singleton; import org.junit.Test; public class LazySimpleSingletonTest { @Test public void getInstance() { Thread t1 = new Thread(new ExecutorThread()); Thread t2 = new Thread(new ExecutorThread()); t1.start(); t2.start(); System.out.println("end"); } }
【2】测试任务类
package com.wf.singleton; /** * @ClassName ExecutorThread * @Description 创建一个测试task * @Author wf * @Date 2020/4/29 18:53 * @Version 1.0 */ public class ExecutorThread implements Runnable { @Override public void run() { LazySimpleSingleton instance = LazySimpleSingleton.getInstance(); System.out.println(Thread.currentThread().getName() +":"+instance); } }
在任务类中,run方法,访问单例类,获取实例。并打印实例hash地址。当两个线程,打印hash地址相同时,
说明只有一个实例,是单例的。
不存在线程安全问题,否则,是线程非安全的。
B.调试过程
【1】直接运行测试类,查看结果:
明显,可以看到,hash地址不同,创建现两个实例。
多次运行,可以发现,有时只会创建一个实例。这时,线程执行的随机性决定的。
我们可以得出结论,getInstance方法是线程非安全的。
【2】断点调试之前,理论上结果分析
从上面的运行,有两种结果:
》同一实例
a,两个线程顺序执行
b.两个线程都进入getInstance方法,if分支【后者覆盖前者】
第一个线程,创建出一个实例instance1
第二个线程,稍后第二个线程,也进入if,并执行new实例instance2,instance2就把instance1给覆盖掉了
最后,得到的实例都是instance2
》不同实例
同时进入if条件,按顺序返回
第一个线程new后,快速执行结束方法,打印实例1
第二个线程,再次new,打印实例2
【3】断点调试
断点设置如下:
注意:
3个断点,都设置成Thread多线程运行模式。设置方法:右击断点,弹出设置框,切换到Thread模式,然后Done.
然后,debug运行测试类:
在Frames窗口下,可以查看到所有的线程。现在,几个线程都是running状态,表示线程都抢到cpu资源,但是什么时候,
执行任务,由cpu来决定。
切换到Thread-0,然后执行下一步,先让它按正常状态执行。
Thread-0线程,先顺序执行完成,并打印.
然后,切换到第二个线程Thread-1,让他进入getInstance方法,经过if判断,由于instance已经创建,非null,条件不成立。
所以,两个线程创建一个实例。如下所示:
可以看到,最后打印相同的结果。
第二种情况 :调试后者覆盖前者
先让两个线程都进入if分支,如下所示:
Thread-0线程进入:
Thread-1线程进入:
然后,再切换到Thread-0,先执行new语句,如下所示:
再切换到,线程1,执行new操作,如下所示:
再切换到,Thread-0执行完成,打印结果。
再让Thread-1执行完成,打印结果:
第三种情况:不同实例
首先,让两个线程都进入getInstance方法的if分支。
然后,先让Thread-0线程执行完,并打印。
最后,Thread-1线程,再执行完成,并打印,如下所示:
出现不同的实例。
2.2.3.解决线程不安全
解决方案:
使用sycronized关键字,加同步锁,如下所示:
package com.wf.singleton; /** * @ClassName LazySimpleSingleton * @Description 懒汉式单例 * @Author wf * @Date 2020/4/29 17:43 * @Version 1.0 */ public class LazySimpleSingleton { private static LazySimpleSingleton instance; private LazySimpleSingleton(){} public synchronized static LazySimpleSingleton getInstance(){//使用synchronized解决线程安全性问题 if(null == instance) { instance = new LazySimpleSingleton(); } return instance; } }
问题解决了。
2.2.3.1.debug分析synchronized关键字作用
先让Thread-0线程进入if分支,如下所示:
可以观察到,两个线程的状态均为Running.
再切换到Thread-1线程,也试图让它进入getInstance方法:
可以发现,Thread-1线程进不去getInstance方法了,并且线程状态变为Monitor.
也就是说Thread-1线程阻塞,开始等待,直到Thread-0线程执行完getInstance方法。
然后,让Thread-0线程执行完,并打印,如下所示:
然后,切换到Thread-1线程,如下所示:
可以发现,当Thread-0线程执行后,切换到Thread-1线程,线程状态又恢复到Running状态,并进入getInstance方法。
所以,我们得出结论:
synchronized关键字,是用来控制资源只能被一个线程占用,实现互斥特性。
虽然,解决了线程安全性问题,但是代码性能受到限制。性能差。
为了解决性能问题,提出“双重检查”单例的解决方案。
2.2.4.双重检查的单例模式
2.2.4.1.代码示例如下:
package com.wf.singleton; /** * @ClassName LazyDoubleCheckSingleton * @Description 双重检查--懒汉式单例 * @Author wf * @Date 2020/4/30 12:41 * @Version 1.0 */ public class LazyDoubleCheckSingleton { //volatile关键字解决指令重排序问题 private volatile static LazyDoubleCheckSingleton instance; private LazyDoubleCheckSingleton(){} public static LazyDoubleCheckSingleton getInstance(){ if(null == instance){//第一次检查,是否阻塞,只阻塞第一次new synchronized (LazyDoubleCheckSingleton.class) { //第二次检查,检查是否重复创建实例 if (null == instance) { instance = new LazyDoubleCheckSingleton(); //会有指令重排序问题 } } } return instance; } }
当在getInstance方法内部使用双重检查机制,解决了懒汉式单例的性能问题。
但它存在指令重排序问题。
创建一个对象包含下面两个过程:
1、类构造器完成类初始化(分配内存、赋予默认值)
2、类实例化(赋予给定值)
在java中,当new一个实例时,会存在两块内存地址的处理:
》创建后的实例会指向堆内存
》成员赋值会指向栈内存
两块内存的操作,先后顺序具有随机性,因此,可能会产生指令重排序问题。
不过,没关系,java提供了volatile关键字,解决指令重排序问题。
2.2.4.2.总结
双重检查机制,解决了线程安全性问题,也提升程序性能。
但是,两个if判断,很容易让人迷惑,可读性差,代码不够优雅。
1.3.2.设计模式03-单例模式【下】
时长:1h6min
2.2.5.懒汉式-静态内部类实现单例
代码如下:
package com.wf.singleton; /** * @ClassName LazyStaticInnerClassSingleton * @Description 懒汉式-静态内部类实现单例 * @Author wf * @Date 2020/4/30 13:11 * @Version 1.0 */ public class LazyStaticInnerClassSingleton { private LazyStaticInnerClassSingleton(){} private static LazyStaticInnerClassSingleton getInstance(){ return LazyHolder.instance; } //静态内部类 private static class LazyHolder{ private static LazyStaticInnerClassSingleton instance = new LazyStaticInnerClassSingleton(); } }
为什么这种方式是懒汉式呢,感觉很类似饿汉式单例?
类加载机制,默认加载classpath下的*.class文件,而内部类在classpath下编译为XXX$LazyHolder.class【主类$内部类.class】
所以,首次加载类时,不会加载内部类。只有当程序中使用到内部类时,才会加载内部类。
总结:
优点:写法优雅,很好利用java本身的语法特点,性能高,避免内存浪费。
缺点:无法避免反射破坏。
实际上,前面的几种单例写法,都无法避免反射破坏。
2.2.6.反射破坏单例测试
为什么反射会破坏单例呢?
因为代码中单例的实现时,基于构造器私有。如果通过反射能拿到类的实例,不就被破坏了吗。
2.2.6.1.测试代码
package com.wf.singleton; import java.lang.reflect.Constructor; /** * @ClassName ReflectTest * @Description 反射破坏单例 * @Author wf * @Date 2020/4/30 13:52 * @Version 1.0 */ public class ReflectTest { public static void main(String[] args) { Class<?> clazz = LazyStaticInnerClassSingleton.class; try { Constructor<?> c = clazz.getDeclaredConstructor(null); System.out.println(c); c.setAccessible(true);//强制 Object obj = c.newInstance();//通过反射,成功绕过单例类的全局访问点,创建实例 Object obj1 = c.newInstance(); System.out.println(obj); System.out.println(obj1); } catch (Exception e) { e.printStackTrace(); } } }
测试结果如下:
很明显,单例特性已经被破坏。
2.2.6.2.如何防止反射破坏单例
解决方法,是在构造器中做非空判断,如果非空,抛异常,提示不允许非法访问。
代码如下:
package com.wf.singleton; /** * @ClassName LazyStaticInnerClassSingleton * @Description 懒汉式-静态内部类实现单例 * @Author wf * @Date 2020/4/30 13:11 * @Version 1.0 */ public class LazyStaticInnerClassSingleton { private LazyStaticInnerClassSingleton(){ //防止反射破坏单例 if(null != LazyHolder.instance){ throw new RuntimeException("不允许非法访问"); } } private static LazyStaticInnerClassSingleton getInstance(){ return LazyHolder.instance; } //静态内部类 private static class LazyHolder{ private static LazyStaticInnerClassSingleton instance = new LazyStaticInnerClassSingleton(); } }
然后,执行测试:
反射破坏单例,成功解决。
总结:
这种在构造器上做判断,抛异常的方式。使代码看起来很奇怪,代码又不够优雅了。程序可读性变差。
那么,什么才是优雅的单例写法呢?
答案是:注册式单例,它是《java Effective》一书中推荐的,最为优雅的单例写法。
3.注册式单例
定义:
将每一个实例都缓存到统一的容器中,使用唯一标识获取实例。
3.1.枚举式单例
枚举式单例,属于一种特殊的注册式单例。
java中提供enum关键字,标识一个类为枚举类。底层它是继承Eunm抽象类。
3.1.1.示例代码如下
package com.wf.singleton.register; /** * @ClassName EnumSingleton * @Description 枚举式单例 * @Author wf * @Date 2020/4/30 14:16 * @Version 1.0 */ public enum EnumSingleton { INSTANCE; //枚举中可以自定义属性 private Object data; public static EnumSingleton getInstance(){ return INSTANCE; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } }
枚举式单例应该怎么使用呢?
创建一个测试类,模拟客户端调用
package com.wf.singleton; import com.wf.singleton.register.EnumSingleton; import java.lang.reflect.Constructor; /** * @ClassName EnumSingletonTest * @Description TODO * @Author wf * @Date 2020/4/30 14:21 * @Version 1.0 */ public class EnumSingletonTest { public static void main(String[] args) { EnumSingleton instance = EnumSingleton.getInstance(); instance.setData("设置值"); //测试反射破坏 Class<?> clazz = EnumSingleton.class; try { Constructor<?> c = clazz.getDeclaredConstructor(null); Object obj1 = c.newInstance(); Object obj2 = c.newInstance(); System.out.println("obj1:"+obj1); System.out.println("obj2:"+obj2); } catch (Exception e) { e.printStackTrace(); } } }
测试结果如下:
可以发现,进行反射破坏时,代码抛异常了。异常信息如下:
java.lang.NoSuchMethodException: com.wf.singleton.register.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.wf.singleton.EnumSingletonTest.main(EnumSingletonTest.java:22)
通过查看源码,Enum类只有一个带2个参数的构造器,那么反射破坏是不是可以基于此创建实例呢,修改代码如下:
package com.wf.singleton; import com.wf.singleton.register.EnumSingleton; import java.lang.reflect.Constructor; /** * @ClassName EnumSingletonTest * @Description TODO * @Author wf * @Date 2020/4/30 14:21 * @Version 1.0 */ public class EnumSingletonTest { public static void main(String[] args) { EnumSingleton instance = EnumSingleton.getInstance(); instance.setData("设置值"); //测试反射破坏 Class<?> clazz = EnumSingleton.class; try { Constructor<?> c = clazz.getDeclaredConstructor(String.class,int.class); System.out.println(c);//可以得到 c.setAccessible(true); Object obj = c.newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
测试结果,报错。提示 不允许反射破坏。
为什么这样呢?意味着枚举类,没有getDeclaredConstructor方法。
3.1.2.查看枚举类的源码Enum
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
可以发现,Enum类中并不存在无参构造,只有一个带2个参数的构造器。
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } //修饰符如果是Enum枚举,不允许反射调用 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); } Map<String, T> enumConstantDirectory() { if (enumConstantDirectory == null) { T[] universe = getEnumConstantsShared(); if (universe == null) throw new IllegalArgumentException( getName() + " is not an enum type"); Map<String, T> m = new HashMap<>(2 * universe.length); for (T constant : universe) m.put(((Enum<?>)constant).name(), constant); enumConstantDirectory = m; } return enumConstantDirectory; } //定义map容器 private volatile transient Map<String, T> enumConstantDirectory = null;
3.1.3.总结
对于枚举来说,只要实例创建完成,就会被当成常量,存入map容器中。
不管用不用,都会存在。
所以,它是一种饿汉式单例。可能造成内存浪费。不能大批量创建对象。
优点:
代码优雅,底层防止反射破坏。线程安全。
因为它不能大量创建对象,所以,在spring中并没有使用枚举来实现单例。
而是基于枚举的设计思想,spring创建ioc容器来实现单例。
3.2.容器式单例
3.2.1.示例代码
package com.wf.singleton; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @ClassName ContainerSingleton * @Description 容器式单例 * @Author wf * @Date 2020/4/30 19:37 * @Version 1.0 */ public class ContainerSingleton { private ContainerSingleton(){} private static Map<String,Object> iocMap = new ConcurrentHashMap<>(); public static Object getInstance(String className){ if(!iocMap.containsKey(className)){ try { Object instance = Class.forName(className).newInstance(); iocMap.put(className,instance); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return iocMap.get(className); } }
3.2.2.测试代码
package com.wf.singleton; /** * @ClassName ContainerSingletonTest * @Description TODO * @Author wf * @Date 2020/4/30 19:42 * @Version 1.0 */ public class ContainerSingletonTest { public static void main(String[] args) { Object inst1 = ContainerSingleton.getInstance("com.wf.singleton.User"); Object inst2 = ContainerSingleton.getInstance("com.wf.singleton.User"); System.out.println(inst1); System.out.println(inst2); } }
package com.wf.singleton; /** * @ClassName User * @Description TODO * @Author wf * @Date 2020/4/30 19:43 * @Version 1.0 */ public class User { }
测试结果,如下:
3.2.3.总结
容器式单例,可以解决大量创建单例对象的问题。但它存在线程安全性问题。
那么,如何解决线程安全性问题呢?【可以从spring中寻找解决方案】,使用synchnonized双重检查锁来解决。
3.3.序列化破坏单例
3.3.1.什么是序列化和反序列化?
把内存中的对象状态,转换为字节码形式,然后再把字节码通过IO流输出,写到磁盘文件,保存到一个文件中。
实现永久化保存的这个过程,就是 序列化。
反之,再次使用时,从磁盘文件,读取内容到内存中,称为反序列化。
3.3.2.序列化破坏单例的代码示例
java中实现序列化,只需要实现Serialzable接口。
package com.wf.singleton; import java.io.Serializable; /** * @ClassName SerializableSingleton * @Description 序列化破坏单例示例 * @Author wf * @Date 2020/4/30 20:03 * @Version 1.0 */ public class SerializableSingleton implements Serializable { private static final SerializableSingleton instance = new SerializableSingleton(); private SerializableSingleton(){} public static SerializableSingleton getInstance(){ return instance; } /* private Object readResolve(){ return instance; }*/ }
创建测试类:
package com.wf.singleton; import java.io.*; /** * @ClassName SerializableSingletonTest * @Description TODO * @Author wf * @Date 2020/4/30 20:08 * @Version 1.0 */ public class SerializableSingletonTest { public static void main(String[] args) { SerializableSingleton s1 = null; SerializableSingleton s2 = SerializableSingleton.getInstance(); FileOutputStream fos = null; try{ fos = new FileOutputStream("serializableSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("serializableSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (SerializableSingleton) ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); System.out.println(s1==s2); }catch (Exception e){ } } }
测试结果如下:
显然,单例被破坏了。
如何解决序列化破坏单例的问题呢,在单例类中增加readResolve方法如下:
package com.wf.singleton; import java.io.Serializable; /** * @ClassName SerializableSingleton * @Description 序列化破坏单例示例 * @Author wf * @Date 2020/4/30 20:03 * @Version 1.0 */ public class SerializableSingleton implements Serializable { private static final SerializableSingleton instance = new SerializableSingleton(); private SerializableSingleton(){} public static SerializableSingleton getInstance(){ return instance; } //防止序列化破坏单例 private Object readResolve(){ return instance; } }
再进行测试,结果如下:
为什么,增加readResolve方法,就解决这个问题了呢?
创建对象,是由java.io.ObjectInputStream#readObject方法来完成的。下面查看它的源码。
3.3.2.readObject源码分析
public final Object readObject() throws IOException, ClassNotFoundException { if (enableOverride) {//允许重写 return readObjectOverride(); } // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(false);//核心逻辑 handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } } private Object readObject0(boolean unshared) throws IOException { boolean oldMode = bin.getBlockDataMode(); if (oldMode) { int remain = bin.currentBlockRemaining(); if (remain > 0) { throw new OptionalDataException(remain); } else if (defaultDataEnd) { /* * Fix for 4360508: stream is currently at the end of a field * value block written via default serialization; since there * is no terminating TC_ENDBLOCKDATA tag, simulate * end-of-custom-data behavior explicitly. */ throw new OptionalDataException(true); } bin.setBlockDataMode(false); } byte tc; while ((tc = bin.peekByte()) == TC_RESET) { bin.readByte(); handleReset(); } depth++; totalObjectRefs++; try { switch (tc) { case TC_NULL: return readNull(); case TC_REFERENCE: return readHandle(unshared); case TC_CLASS: return readClass(unshared); case TC_CLASSDESC: case TC_PROXYCLASSDESC: return readClassDesc(unshared); case TC_STRING: case TC_LONGSTRING://checkResovle方法 return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); // force header read throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); } case TC_ENDBLOCKDATA: if (oldMode) { throw new OptionalDataException(true); } else { throw new StreamCorruptedException( "unexpected end of block data"); } default: throw new StreamCorruptedException( String.format("invalid type code: %02X", tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } } private Object readOrdinaryObject(boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); desc.checkDeserialize(); Class<?> cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException("invalid class descriptor"); } Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } passHandle = handles.assign(unshared ? unsharedMarker : obj); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(passHandle, resolveEx); } if (desc.isExternalizable()) { readExternalData((Externalizable) obj, desc); } else { readSerialData(obj, desc); } handles.finish(passHandle); if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj);//核心逻辑 if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } } return obj; } Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException { requireInitialized(); if (readResolveMethod != null) { try {//如果有readResolve方法,就会调用readResolve的结果 return readResolveMethod.invoke(obj, (Object[]) null); } catch (InvocationTargetException ex) { Throwable th = ex.getTargetException(); if (th instanceof ObjectStreamException) { throw (ObjectStreamException) th; } else { throwMiscException(th); throw new InternalError(th); // never reached } } catch (IllegalAccessException ex) { // should not occur, as access checks have been suppressed throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } }
当不存在readResolve方法时,就会执行newInstance方法,创建实例,导致单例被坏
4.ThreadLocal单例
ThreadLocal线程局部,创建一个与线程绑定的变量,所以,它能隔离线程。
它天生能保证线程安全。
4.1.示例代码
package com.wf.singleton.threadlocal; /** * @ClassName ThreadLocalSingleton * @Description TODO * @Author wf * @Date 2020/5/6 12:18 * @Version 1.0 */ public class ThreadLocalSingleton { private static final ThreadLocal<ThreadLocalSingleton> theadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){ @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton(){} public static ThreadLocalSingleton getInstance(){ return theadLocalInstance.get(); } }
测试代码:
package com.wf.singleton; import com.wf.singleton.threadlocal.ThreadLocalSingleton; /** * @ClassName ThreadLocalSingletonTest * @Description TODO * @Author wf * @Date 2020/5/6 12:24 * @Version 1.0 */ public class ThreadLocalSingletonTest { public static void main(String[] args) { System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); } }
测试结果如下:
多次获取实例,得到同一实例。显然是满足单例的。
但是,ThreadLocal有其特殊性,它是线程绑定的。即不同线程获得实例,可能是不同的。
4.1.1.ThreadLocal单例特殊性测试
package com.wf.singleton; import com.wf.singleton.threadlocal.ThreadLocalSingleton; /** * @ClassName ThreadLocalSingletonTest * @Description TODO * @Author wf * @Date 2020/5/6 12:24 * @Version 1.0 */ public class ThreadLocalSingletonTest { public static void main(String[] args) { System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); Thread t1 = new Thread(()->{ System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance()); }); Thread t2 = new Thread(() ->{ System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance()); }); t1.start(); t2.start(); System.out.println("end"); } }
测试结果如下:
说明:
似乎是产生了三个不同的实例。似乎是破坏了单例。但这是正常的。
ThreadLocal只能保证同一线程下,只有一个实例。
所以,这种单例是一种局限性单例。有其特殊的应用场景。
4.2.ThreadLocal的原理探究
核心方法:get()
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);//获取map if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//key是this,表示当前对象所在线程 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
线程的原因,是内部存在一个map维护数据,key是当前对象的所在的线程。
5.单例在源码中应用
spring中AbstractFactoryBean#getObject
Mybatis中ErrorContext[ThreadLocal的应用]
6.单例模式的总结
6.1.优点
在内存中只有一个实例,减少了内存开销。
可以避免对资源的多重占用。
设置全局访问点,严格控制访问。
6.2.缺点
不能面向接口编程。扩展困难。
如果要扩展单例对象,只能修改代码。【某种意义上说,违背开闭原则】
6.3.相关知识点总结
1.构造器私有
2.保证线程安全性
3.延迟加载【懒汉式--使用才创建】
4.防止序列化和反序列化破坏单例
5.防御反射破坏单例
6.4.补充知识---容器式单例的线程安全性解决
AbstractBeanFactory----getBean--->doGetBean--->getObjectForBeanInstance--->getObjectFromFactoryBean
可以知道,使用synchronized同步锁
附录:
idea自动生成junit测试类:
两种快捷键:
1.在要生成测试类的类里面,按ctrl+shift+t
–> create new test