浅谈Java常见设计模式(二)
你,发如雪,凄美了离别
我焚香感动了谁
邀明月,让回忆皎洁
爱在月光下完美
你,发如雪,纷飞了眼泪
我等待苍老了谁
红尘醉,微醺的岁月
我用无悔,刻永世爱你的碑
接浅谈Java常见设计模式(一),上文说到了关于如何创建城市的对比信息,一般来说,对比城市,需要创建所有的信息保证唯一性,以免到时候维度不唯一了,导致对比性出现了差异。比如,如果创建城市基本信息,那么关于每一个城市,只需要创建出唯一的一个就好了,这时候,就需要用到单例模式,即在程序运行时,对象只有一份!
一般来说,单例模式具有4种实现方式:
饿汉式单例:
饿汉式单例,即对象被加载到内存中后,初始化改对象,然后使用的时候直接获取:
package com.lgli.create.single;/*** 单例模式* @author lgli*/public class Single {public static void main(String[] args) {ChongQingCityBase base = ChongQingCityBase.getInstance();System.out.println(base);}}/*** 重庆市基本信息* @author lgli*/class ChongQingCityBase extends CityBase {private static final ChongQingCityBase chongQingCityBase= new ChongQingCityBase();private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return chongQingCityBase;}void base() {System.out.println("重庆市基本信息");}}/*** 抽象城市基本信息类*/abstract class CityBase{abstract void base();}
这里用多线程来测试下,是否都只实例化了一个对象:

这里定义了1000个线程运行,得到的对象都是一个
饿汉式单例,是无论是否需要用到,在类被加载到内存中的时候,就已经在堆中实例化了这个对象,
这时候,就有点尴尬了,假设从来没有用到过,那么这就有点浪费内存了,
所以,懒汉式单例应运而生
懒汉式单例:
懒汉式单例,是需要用到这个对象的时候,才实例化这个对象
package com.lgli.create.single.singlev2;/**** 单例模式--懒汉式* @author lgli*/public class Single {public static void main(String[] args) {ChongQingCityBase base = ChongQingCityBase.getInstance();System.out.println(base);}}/*** 重庆市基本信息* @author lgli*/class ChongQingCityBase extends CityBase {private static ChongQingCityBase chongQingCityBase;private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){if(chongQingCityBase == null){chongQingCityBase = new ChongQingCityBase();}return chongQingCityBase;}}/*** 抽象城市基本信息类*/abstract class CityBase{}
这里先不初始化对象,当调用getInstance方法的时候,才初始化这个对象,同时判断当前对象是否存在,如果存在,则直接返回
这种情况,在单线程的情况下,是没有什么问题的,可是当遇到多个线程并发的时候,就出现问题了:

如上图所示,这里用100个线程模拟并发,出现了多个实例
来看下这个代码:

分析下,并发的情况下,
当某个线程执行到44行的时候,这个时候chongQingCityBase是null的,此时,线程切换,另外一个线程也走到44行,判断也是为空的,那么这2个线程,均符合if判断,去new一个对象,这时候就出现了多个实例的情况。
那么解决这个问题吧
首先想到的就是加锁吧,首先可能想到把这个实例化方法加锁,这样子肯定是可能满足只产生一个对象的,因为毕竟方法锁的话,多个线程都会等待方法锁的释放才能调用这个方法。

如果,实例化方法里面可能还有其他逻辑,这样子暴力加锁肯定不是一个很好的解决方案,
所以只需要在关键的地方加锁:
比如:

这里将加锁加到初始化实例对象的地方,然后同样的用多线程去运行下结果,这里为了解决可能因为多线程导致JVM指令重排<JVM中将不影响单线程代码实际效果的指令下,颠倒执行顺序>出错,这里将这个成员变量加上volatile。

这里可以看见,依然有多个实例的产生,即此种加锁的方式,是存在问题的,可能导致初始化多个对象。
简单分析下原因:
当多个线程同时走过if判断,此时chongQingCityBase是null的,然后进入到加锁的方法,这时候出现的结果就是会有多个对象被实例化了
即:

线程1和2在chongQingCityBase为null时,同时进入if判断。
如何解决这个问题呢?
这就是懒汉式加载的双重判断机制,即:

这里加了双重的校验,即在加锁的方法内部,也同样执行判断。
此时,看下运行结果<记住这里的volatile关键字,否则也是有可能出现多个实例化对象的,具体原因,后续会说到>:

此时,实例化的对象就只有一个了,
那么这个地方有个问题就是,这个双重判断外面这个判断的意义是什么呢?
如果没有外面的判断,意味着每次执行方法的时候,都会进去到线程等待的方法,和在方法上加锁没有太大的区别,所以为了性能考虑,加上这个外层判断
内部类单例:
看下面代码:
package com.lgli.create.single.singlev3;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/*** 单例模式 -- 内部类* @author lgli*/public class Single {public static void main(String[] args) {ExecutorService executorService = Executors.newCachedThreadPool();for(int i = 0 ; i < 100 ; i++){executorService.execute(()->{ChongQingCityBase base = ChongQingCityBase.getInstance();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().toString()+base);});}executorService.shutdown();}}/*** 重庆市基本信息* @author lgli*/class ChongQingCityBase extends CityBase {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBaseHolder.CHONGQINGCITYBASE;}private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息类*/abstract class CityBase{}
运行结果,也是只有一个实例的产生,
那么这种静态内部类的实现原理是什么样子的呢?
涉及到JVM类加载的一些逻辑,大致情况是这样子的:
当ChongQingCityBase被加载的时候,会优先加载内部类
ChongQingCityBaseHolder
当有程序调用
com.lgli.create.single.singlev3.ChongQingCityBase#getInstance
方法的时候,
执行
ChongQingCityBaseHolder.CHONGQINGCITYBASE
此时才会去执行内部类的
private static final ChongQingCityBase CHONGQINGCITYBASE = new ChongQingCityBase();
所以这里的内部类方式,其实也属于懒汉式加载
暴力破坏单例-->反射:
有时候,可能一个单例对象,会被人误用反射机制暴力破解,创建出多个实例,这里以为内部类为例,代码指出反射暴力创建对象导致单例失效的情况,其他懒汉/饿汉类似也可以通过此方法破解:
package com.lgli.create.single.breaksingle;import java.lang.reflect.Constructor;/*** 反射破坏单例* @author lgli*/public class BreakSingle {public static void main(String[] args) throws Exception {//反射创建对象//根据对象路径获取类Class<?> aClass = Class.forName("com.lgli.create.single.breaksingle.ChongQingCityBase");//获取申明的没有参数的构造方法Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(null);//设置强访问declaredConstructor.setAccessible(true);//获取实例对象ChongQingCityBase o = (ChongQingCityBase)declaredConstructor.newInstance();//正常创建对象ChongQingCityBase p = ChongQingCityBase.getInstance();System.out.println(o);System.out.println(p);System.out.println(o==p);}}/*** 重庆市基本信息* @author lgli*/class ChongQingCityBase extends CityBase {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}/*** 内部类的单例*/private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息类*/abstract class CityBase{}
运行可以获得两个不同的对象:

这里可以看到通过反射实例化了一个对象,然后通过正常创建又获取了一个对象。
那么如何规避这个问题呢?
可以在构造方法中加入判断:
比如:

可以直接在构造方法中,抛出异常,不允许构建实例。
这时候,反射机制构建实例就会抛出异常,只能正常的构建了。

除此之外,还可以通过序列化的方式,破坏单例
暴力破坏单例-->序列化:
看下面代码:
package com.lgli.create.single.breaksinglev2;import java.io.*;/*** 序列化破坏单例* @author lgli*/public class SerializableSingle {public static void main(String[] args) {//根据正常构建方法获取的对象ChongQingCityBase base1 = ChongQingCityBase.getInstance();//序列化后反序列化的对象ChongQingCityBase base2 = null;try{//序列化对象FileOutputStream fileOutputStream = new FileOutputStream("../ChongQingCityBase.obj");ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);objectOutputStream.writeObject(base1);objectOutputStream.flush();objectOutputStream.close();fileOutputStream.close();//反序列化实例对象FileInputStream fileInputStream = new FileInputStream("../ChongQingCityBase.obj");ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);base2 = (ChongQingCityBase)objectInputStream.readObject();fileInputStream.close();objectInputStream.close();//对比两个结果对象System.out.println(base1);System.out.println(base2);System.out.println(base1 == base2);}catch (Exception e){e.printStackTrace();}}}/*** 重庆市基本信息* @author lgli*/class ChongQingCityBase extends CityBase implements Serializable {private ChongQingCityBase (){if(ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE != null){throw new RuntimeException("不允许构建多个实例");}}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}/*** 内部类的单例*/private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息类*/abstract class CityBase{}
运行代码结果输出:

那么针对这种破坏单例的方法,如何预防呢?
这时候需要在实例中写个方法:

如上图所示,这里需要写这个readResolve方法,
这时候运行程序:

显然,这个时候,只获取了一个实例。
那么这个是为啥呢?
这里简单描述下,画个图

这里是反序列化的时候,调用对象创建方法的源码,在
java.io.ObjectInputStream#readObject0
调用
java.io.ObjectInputStream#readOrdinaryObject
之后,会返回这个创建的对象
那么看下
java.io.ObjectInputStream#readOrdinaryObject
这个方法究竟做了些什么事呢?
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 objectif (rep != null) {if (rep.getClass().isArray()) {filterCheck(rep.getClass(), Array.getLength(rep));} else {filterCheck(rep.getClass(), -1);}}handles.setObject(passHandle, obj = rep);}}return obj;}
上面代码是JDK源码,这里主要看关键部分

这里先判断是否满足isInstantiable()
这个方法其实是判断是否有构造方法的,这里就不往下看了
很明显,是有构造方法的,虽然是private的,但是对JDK来说,这都不重要
然后满足有构造方法,则创建实例对象,这时候创建的实例对象和正常创建的肯定是两回事。所以序列化再反序列化后的对象,肯定不是同一个了,这里就理解到了
再接着看下面的代码:

这里紧接着上面的代码走,有一个判断,然后满足判断之后,则调用
java.io.ObjectStreamClass#invokeReadResolve
方法,实例化新的对象出来,
当对象写了readResolve方法后,就满足了这里的if条件,然后会调用对象中的readResolve方法,而我们写的readResolve方法就是返回单例对象,所以这里同样返回了单例对象,覆盖了前面所创建的实例对象:

所以得到的对象是一个单例对象。
枚举单例-->最牛单例写法
最后介绍一个Java推荐的单例模式的写法,枚举单例
看下代码:
package com.lgli.create.single.singlev4;import java.io.*;/**** 枚举单例* @author lgli*/public class Single {public static void main(String[] args) {//根据正常构建方法获取的对象EnumSingle base1 = EnumSingle.getInstance();base1.setObj(ChongQingCityBase.getInstance());//序列化后反序列化的对象EnumSingle base2 = null;try{//序列化对象FileOutputStream fileOutputStream = new FileOutputStream("../ChongQingCityBase.obj");ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);objectOutputStream.writeObject(base1);objectOutputStream.flush();objectOutputStream.close();fileOutputStream.close();//反序列化实例对象FileInputStream fileInputStream = new FileInputStream("../ChongQingCityBase.obj");ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);base2 = (EnumSingle)objectInputStream.readObject();fileInputStream.close();objectInputStream.close();System.out.println(base1.getObj());//对比两个结果对象System.out.println(base2.getObj());System.out.println(base1 == base2);}catch (Exception e){e.printStackTrace();}}}/**** 枚举单例*/enum EnumSingle{INSTANCE;private Object obj;public Object getObj() {return obj;}public void setObj(Object obj) {this.obj = obj;}public static EnumSingle getInstance(){return INSTANCE;}}/*** 重庆市基本信息* @author lgli*/class ChongQingCityBase extends CityBase implements Serializable {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息类*/abstract class CityBase{}
这里,申明了一个枚举,
同时枚举类中有一个成员变量Object,
用来设置初始化的单例对象的
然后先测试下,这里通过序列化破坏单例,是否可以成功,
运行上述代码:

这里,可以很明显的看到,两个对象实例是一样的,即,在通过序列化,同时没有写readResolve方法,依然获得了两个一样的对象,即序列化和反序列化并没有破坏枚举单例。
为何枚举单例可以防止序列化呢?
同样的,看下反序列化时,创建对象的时候,做了些什么事
和前面的逻辑调用图基本一致,只不过在最后一个是调用
java.io.ObjectInputStream#readEnum
源码如下:

这里,可以看到返回的对象是通过一个类名,和一个唯一的
名字name,构建了这个返回对象,很显然,在同一个应用中,这是唯一的,
所以构建出来的对象也是唯一的。
那么线程安全么?
这里使用反编译工具,将写的这个枚举单例类反编译出来,这里使用的是JAD,其安装使用就不多说了,记住一点就好,在window环境下,需要把jad.exe放到本机jdk的bin文件下
反编译这个EnumSingle.class

在当前目录下生成了一个EnumSingle.jad文件
打开这个文件:

关键位置,如上图后面一个图标识所示,这种方式属于饿汉式单例,所以一定是线程安全的
然后又不能通过反序列化
那么反射呢?
测试下反射,这里使用反射获取构造方法是时候,获取到的应该是带2个参数的构造方法,String和int,上面截图标识有构造方法。
看下代码:
package com.lgli.create.single.singlev4;import java.io.*;import java.lang.reflect.Constructor;/**** 枚举单例* @author lgli*/public class Single {public static void main(String[] args) {// serializableType();reflectType();}/*** 反射方式*/public static void reflectType(){try{Class<?> aClass = Class.forName("com.lgli.create.single.singlev4.EnumSingle");Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(String.class,int.class);declaredConstructor.setAccessible(true);EnumSingle lgli = (EnumSingle)declaredConstructor.newInstance("lgli", 520);System.out.println(lgli);}catch (Exception e){e.printStackTrace();}}/*** 序列化和反序列化破幻枚举单例方式*/public static void serializableType(){//根据正常构建方法获取的对象EnumSingle base1 = EnumSingle.getInstance();base1.setObj(ChongQingCityBase.getInstance());//序列化后反序列化的对象EnumSingle base2 = null;try{//序列化对象FileOutputStream fileOutputStream = new FileOutputStream("../ChongQingCityBase.obj");ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);objectOutputStream.writeObject(base1);objectOutputStream.flush();objectOutputStream.close();fileOutputStream.close();//反序列化实例对象FileInputStream fileInputStream = new FileInputStream("../ChongQingCityBase.obj");ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);base2 = (EnumSingle)objectInputStream.readObject();fileInputStream.close();objectInputStream.close();System.out.println(base1.getObj());//对比两个结果对象System.out.println(base2.getObj());System.out.println(base1 == base2);}catch (Exception e){e.printStackTrace();}}}/**** 枚举单例*/enum EnumSingle{INSTANCE;private Object obj;public Object getObj() {return obj;}public void setObj(Object obj) {this.obj = obj;}public static EnumSingle getInstance(){return INSTANCE;}}/*** 重庆市基本信息* @author lgli*/class ChongQingCityBase extends CityBase implements Serializable {private ChongQingCityBase (){}public static ChongQingCityBase getInstance(){return ChongQingCityBase.ChongQingCityBaseHolder.CHONGQINGCITYBASE;}private static class ChongQingCityBaseHolder{private static final ChongQingCityBaseCHONGQINGCITYBASE = new ChongQingCityBase();}}/*** 抽象城市基本信息类*/abstract class CityBase{}
运行测试类,结果报错了:

程序异常:Cannot reflectively create enum objects
无法通过反射创建枚举对象
。。。。。。
这说明什么呢?说明JDK在从根本上解决了妄图通过反射去构建枚举对象的幻想!
看下
java.lang.reflect.Constructor#newInstance
源码:

所以,反射是不可行的。
总结起来,通过枚举创建单例,无论是多线程、序列化、反射都无法改变单例,所以枚举应该是最牛的吧。(#^.^#)
其实还要一种单例,属于线程内单例,但是线程之间不是单例的-->
ThreadLocal
这里就先不详细解释了,后续会说到这个问题
欢迎转发关注,谢谢!
点个赞咯!
