浅谈Java常见设计模式(二)
共 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();
}
这里用多线程来测试下,是否都只实例化了一个对象:
这里定义了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 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{
}
运行可以获得两个不同的对象:
这里可以看到通过反射实例化了一个对象,然后通过正常创建又获取了一个对象。
那么如何规避这个问题呢?
可以在构造方法中加入判断:
比如:
可以直接在构造方法中,抛出异常,不允许构建实例。
这时候,反射机制构建实例就会抛出异常,只能正常的构建了。
除此之外,还可以通过序列化的方式,破坏单例
暴力破坏单例-->序列化:
看下面代码:
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{
}
运行代码结果输出:
那么针对这种破坏单例的方法,如何预防呢?
这时候需要在实例中写个方法:
如上图所示,这里需要写这个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 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源码,这里主要看关键部分
这里先判断是否满足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 ChongQingCityBase
CHONGQINGCITYBASE = 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 ChongQingCityBase
CHONGQINGCITYBASE = new ChongQingCityBase();
}
}
/**
* 抽象城市基本信息类
*/
abstract class CityBase{
}
运行测试类,结果报错了:
程序异常:Cannot reflectively create enum objects
无法通过反射创建枚举对象
。。。。。。
这说明什么呢?说明JDK在从根本上解决了妄图通过反射去构建枚举对象的幻想!
看下
java.lang.reflect.Constructor#newInstance
源码:
所以,反射是不可行的。
总结起来,通过枚举创建单例,无论是多线程、序列化、反射都无法改变单例,所以枚举应该是最牛的吧。(#^.^#)
其实还要一种单例,属于线程内单例,但是线程之间不是单例的-->
ThreadLocal
这里就先不详细解释了,后续会说到这个问题
欢迎转发关注,谢谢!
点个赞咯!