浅谈Java常见设计模式(二)

lgli

共 16127字,需浏览 33分钟

 ·

2021-02-05 12:26

你,发如雪,凄美了离别

我焚香感动了谁

邀明月,让回忆皎洁

爱在月光下完美

你,发如雪,纷飞了眼泪

我等待苍老了谁

红尘醉,微醺的岁月

我用无悔,刻永世爱你的碑



浅谈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();}


这里用多线程来测试下,是否都只实例化了一个对象:


0366d3336917a105b1cf1fb0b6960149.webp


这里定义了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方法的时候,才初始化这个对象,同时判断当前对象是否存在,如果存在,则直接返回


这种情况,在单线程的情况下,是没有什么问题的,可是当遇到多个线程并发的时候,就出现问题了:


8b9ebe85a5482dd458063270f4518caf.webp


如上图所示,这里用100个线程模拟并发,出现了多个实例


来看下这个代码:


95d07f33abb835d5230bb5c5655bc54e.webp


分析下,并发的情况下,


当某个线程执行到44行的时候,这个时候chongQingCityBase是null的,此时,线程切换,另外一个线程也走到44行,判断也是为空的,那么这2个线程,均符合if判断,去new一个对象,这时候就出现了多个实例的情况。


那么解决这个问题吧


首先想到的就是加锁吧,首先可能想到把这个实例化方法加锁,这样子肯定是可能满足只产生一个对象的,因为毕竟方法锁的话,多个线程都会等待方法锁的释放才能调用这个方法。


4a9393d18a127ddd9c6cf7952a61da22.webp


如果,实例化方法里面可能还有其他逻辑,这样子暴力加锁肯定不是一个很好的解决方案,


所以只需要在关键的地方加锁:


比如:


d816f5fac58c66ba10446be440612d3a.webp


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


5e632b222d1eebe7e9209485d63be2a0.webp



这里可以看见,依然有多个实例的产生,即此种加锁的方式,是存在问题的,可能导致初始化多个对象。


简单分析下原因:


当多个线程同时走过if判断,此时chongQingCityBase是null的,然后进入到加锁的方法,这时候出现的结果就是会有多个对象被实例化了


即:


e8603ae0d2ce3c97180919d490721630.webp



线程1和2在chongQingCityBase为null时,同时进入if判断。




如何解决这个问题呢?



这就是懒汉式加载的双重判断机制,即:


c8ff8aa87e0488d96e72ebee1e12c1fa.webp


这里加了双重的校验,即在加锁的方法内部,也同样执行判断。


此时,看下运行结果<记住这里的volatile关键字,否则也是有可能出现多个实例化对象的,具体原因,后续会说到>:


052a9ccf529ec41d28b19d48be9439eb.webp


此时,实例化的对象就只有一个了,

那么这个地方有个问题就是,这个双重判断外面这个判断的意义是什么呢?


如果没有外面的判断,意味着每次执行方法的时候,都会进去到线程等待的方法,和在方法上加锁没有太大的区别,所以为了性能考虑,加上这个外层判断


内部类单例:


看下面代码:


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 ChongQingCityBase CHONGQINGCITYBASE = 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 ChongQingCityBase CHONGQINGCITYBASE = new ChongQingCityBase(); }}
/** * 抽象城市基本信息类 */abstract class CityBase{
}



运行可以获得两个不同的对象:


f3682c7f90a35095d02afcd43f790ce4.webp


这里可以看到通过反射实例化了一个对象,然后通过正常创建又获取了一个对象。


那么如何规避这个问题呢?




可以在构造方法中加入判断:



比如:


1b9b95969b972d3dbfca2e8d8eae0d03.webp

可以直接在构造方法中,抛出异常,不允许构建实例。


这时候,反射机制构建实例就会抛出异常,只能正常的构建了。


319275715d1b85d06a926fb63ee4aa17.webp


除此之外,还可以通过序列化的方式,破坏单例


暴力破坏单例-->序列化


看下面代码:


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 ChongQingCityBase CHONGQINGCITYBASE = new ChongQingCityBase(); }}
/** * 抽象城市基本信息类 */abstract class CityBase{
}


运行代码结果输出:


e8395167c7f13b52ad5e556c241e7be8.webp



那么针对这种破坏单例的方法,如何预防呢?


这时候需要在实例中写个方法:


b5715cb2e0a7174abc576a6b2dcbf6f0.webp


如上图所示,这里需要写这个readResolve方法,


这时候运行程序:


727c9fbe4f2883f341382b0af025fd4c.webp


显然,这个时候,只获取了一个实例。


那么这个是为啥呢?


这里简单描述下,画个图


1471d1ee2d42c28c00f156d1a82c1267.webp

这里是反序列化的时候,调用对象创建方法的源码,在

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 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; }


上面代码是JDK源码,这里主要看关键部分


b3d786f2d5d8c81abfbce0c3e1f1e9b6.webp



这里先判断是否满足isInstantiable()


这个方法其实是判断是否有构造方法的,这里就不往下看了


很明显,是有构造方法的,虽然是private的,但是对JDK来说,这都不重要



然后满足有构造方法,则创建实例对象,这时候创建的实例对象和正常创建的肯定是两回事。所以序列化再反序列化后的对象,肯定不是同一个了,这里就理解到了



再接着看下面的代码:


3a8e7b61222b5c8d3252ff5965a71925.webp


这里紧接着上面的代码走,有一个判断,然后满足判断之后,则调用

java.io.ObjectStreamClass#invokeReadResolve

方法,实例化新的对象出来,


当对象写了readResolve方法后,就满足了这里的if条件,然后会调用对象中的readResolve方法,而我们写的readResolve方法就是返回单例对象,所以这里同样返回了单例对象,覆盖了前面所创建的实例对象:


c4dd458a2514845da5484c5e9e647352.webp


所以得到的对象是一个单例对象。



枚举单例-->最牛单例写法


最后介绍一个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 ChongQingCityBase CHONGQINGCITYBASE = new ChongQingCityBase(); }}
/** * 抽象城市基本信息类 */abstract class CityBase{
}


这里,申明了一个枚举,

同时枚举类中有一个成员变量Object,

用来设置初始化的单例对象的



然后先测试下,这里通过序列化破坏单例,是否可以成功,


运行上述代码:


2f33a727b5091b5642155109a072739b.webp


这里,可以很明显的看到,两个对象实例是一样的,即,在通过序列化,同时没有写readResolve方法,依然获得了两个一样的对象,即序列化和反序列化并没有破坏枚举单例。



为何枚举单例可以防止序列化呢?



同样的,看下反序列化时,创建对象的时候,做了些什么事


和前面的逻辑调用图基本一致,只不过在最后一个是调用

java.io.ObjectInputStream#readEnum


源码如下:


d0e2cc08dcf38e2042bc2415c25d0bbf.webp



这里,可以看到返回的对象是通过一个类名,和一个唯一的

名字name,构建了这个返回对象,很显然,在同一个应用中,这是唯一的,

所以构建出来的对象也是唯一的。


那么线程安全么?


这里使用反编译工具,将写的这个枚举单例类反编译出来,这里使用的是JAD,其安装使用就不多说了,记住一点就好,在window环境下,需要把jad.exe放到本机jdk的bin文件下


反编译这个EnumSingle.class


1a5462e486f398df771f600e1e84adf4.webp


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


打开这个文件:


7da71458d69c6b99ae2ce3ff5366ef36.webp


关键位置,如上图后面一个图标识所示,这种方式属于饿汉式单例,所以一定是线程安全的


然后又不能通过反序列化


那么反射呢?


测试下反射,这里使用反射获取构造方法是时候,获取到的应该是带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 ChongQingCityBase CHONGQINGCITYBASE = new ChongQingCityBase(); }}
/** * 抽象城市基本信息类 */abstract class CityBase{
}


运行测试类,结果报错了:


c2ec1f72f9843b5ffe663fd4beb308ca.webp

程序异常:Cannot reflectively create enum objects


无法通过反射创建枚举对象


。。。。。。


这说明什么呢?说明JDK在从根本上解决了妄图通过反射去构建枚举对象的幻想!


看下

java.lang.reflect.Constructor#newInstance

源码:


70d185e70efc9fc8bc58a6627d8fedac.webp


所以,反射是不可行的。


总结起来,通过枚举创建单例,无论是多线程、序列化、反射都无法改变单例,所以枚举应该是最牛的吧。(#^.^#)


其实还要一种单例,属于线程内单例,但是线程之间不是单例的-->

ThreadLocal


这里就先不详细解释了,后续会说到这个问题



欢迎转发关注,谢谢!



点个赞咯!

浏览 9
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报