7、常用的设计模式(代表了最佳实践 共23种,常用的14种)
掌握设计模式的五个层次
第一层:刚开始学编程不久,听说过什么是设计模式;
第二层:有长时间的编程经验,自己写了很多代码,其中用到了设计模式,但是自己却不知道;
第三层:学习过了设计模式,发现自己已经在使用了,并且发现一些新的模式挺好用的;✅
第四层:阅读了很多别人写的源码和框架, 在其中看到别人设计模式,并且能够领会设计模式的精妙和带来的好处
第五层: 代码写着写着,自己都没有意识到使用了设计模式,并且熟练的写了出来
总体来说设计模式分为三大类:
创建型模式(对对象创建过程中各种问题和解决方案的总结)
- 共5种:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)
结构型模式(关注于类/对象 继承/组合方式)
- 共7种:代理模式,桥接模式,适配器模式,装饰器模式,外观模式(不常用),组合模式(不常用),享元模式(不常用)
行为型模式(是从类或对象之间交互/职责划分等角度总结的模式)
- 共11种:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)
- 总结
- 设计模式要干的事情就是解耦。
创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。
8、创建型设计模式
创建型设计模式包括:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码。
8.1、单例设计模式一共有几种实现方式?请分别用代码实现,并说明各个实现方式的优点和缺点?
单例模式用来创建全局唯一的对象。
定义:一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式(两种)、懒汉式(三种)、双重检测、静态内部类、枚举(最佳实践)。
使用场景:①需要频繁的进行创建和销毁的对象、②创建对象时耗时过多或耗费资源过多(即:重量级对象), 但又经常用到的对象、③工具类对象、④频繁访问数据库或文件的对象(比如数据源、 session工厂等)
Demo1、饿汉式:(关键词:静态常量)
public Class singleton{ // 1、私有化构造函数 private Singleton() {} // 2、内部直接创建对象 public static singleton instance = new singleton(); // 3、提供公有静态方法,返回对象实例 public static Singleton getInstance() { return instance; } } 优点:写法简单,避免线程同步问题 缺点:没有懒加载,可能造成内存浪费 不推荐. 使用场景:耗时的初始化操作,提前到程序启动时完成
Demo2、饿汉式(静态代码块)
public Class singleton{ // 1、私有化构造函数 private Singleton() {} //2.本类内部创建对象实例 private static Singleton instance; static { // 在静态代码块中,创建单例对象 instance = new Singleton(); } //3. 提供一个公有的静态方法,返回实例对象 public static Singleton getInstance() { return instance; } } 优点:写法简单,避免线程同步问题 缺点:没有懒加载,可能造成内存浪费 不推荐
Demo3、懒汉式(线程不安全)
class Singleton { private static Singleton instance; private Singleton() {} //提供一个静态的公有方法,当使用到该方法时,才去创建 instance public static Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } } 优点:起到了懒加载的效果 缺点:只能在单线程环境使用, 不要使用
Action1:为什么懒汉单例在多线程环境下会有线程安全问题呢?
在多线程环境下,显然不能保证单例。
- 如果多个线程能够同时进入
if (instance == null)
,并且此时 instance 为 null,那么会有多个线程执行instance = new SingletonDemo();
语句,这将导致多次实例化 instance。
public class SingletonDemo { private static SingletonDemo instance = null; private SingletonDemo () { System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo"); } public static SingletonDemo getInstance() { if(instance == null) { instance = new SingletonDemo(); } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> { SingletonDemo.getInstance(); }, String.valueOf(i)).start(); } } }
输出结果:
4 我是构造方法SingletonDemo 2 我是构造方法SingletonDemo 5 我是构造方法SingletonDemo 6 我是构造方法SingletonDemo 0 我是构造方法SingletonDemo 3 我是构造方法SingletonDemo 1 我是构造方法SingletonDemo
解决方法之一:
- 用
synchronized
修饰方法getInstance()
,但它属重量级同步机制,使用时慎重。
Demo4、懒汉式(线程安全)
class Singleton { private static Singleton instance; private Singleton() {} //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题 public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } } 优点:起到了懒加载的效果,解决了线程安全问题 缺点:效率低,不推荐
Demo5、懒汉式(线程安全,同步代码块)
class Singleton { private static Singleton singleton; private Singleton() {} //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题 public static Singleton getInstance() { if(singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); } return singleton; } } 不推荐使用,并不能起到线程同步的作用
Demo6、双重锁校验 可以应用于连接池的使用中
- DCL中volatile解析
- 原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance 的引用对象可能没有完成初始化。
instance = new SingletonDemo();
可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间 instance(memory); //2.初始化对象 instance = memory; //3.设置instance指向刚分配的内存地址,此时instance != null
- 步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
- 但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
- 所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。
memory = allocate(); //1.分配对象内存空间 instance = memory;//3.设置instance指向刚分配的内存地址,此时instance! =null,但是对象还没有初始化完成! instance(memory);//2.初始化对象
public Class singleton{ private static volatile Singleton instance; private Singleton() {} //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题 //同时保证了效率, 推荐使用 public static Singleton getInstance() { if(instance == null) { synchronized (Singleton.class) { if(instance == null) { instance = new Singleton(); } } } return instance; } } // 优点:Double-Check机制保证线程安全, // 加锁操作只需要对实例化那部分的代码进行,只有当 instance 没有被实例化时,才需要进行加锁。效率高 // 因此推荐使用
问题1:如果只使用了一个 if 语句。在 instance == null 的情况下,如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 instance = new Singleton();
这条语句,只是先后的问题,那么就会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。
问题2:instance 采用 volatile 关键字修饰也是很有必要的。instance = new Singleton();
这段代码其实是分为三步执行。
- 分配内存空间
- 初始化对象
- 将 instance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2,这在单线程情况下自然是没有问题。但如果是多线程下,有可能获得是一个还没有被初始化的实例,以致于程序出错。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
Demo7、静态内部类
class Singleton { private static Singleton instance; //构造器私有化 private Singleton() {} //写一个静态内部类,该类中有一个静态属性 Singleton private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } //提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE public static Singleton getInstance() { return SingletonInstance.INSTANCE; } } 优点:JVM保证了线程安全,类的静态属性只会在第一次加载类的时候初始化,效率高,推荐
Demo8、枚举 – 单例实现的最佳实践
enum Singleton { INSTANCE; //属性 public void sayOK() { System.out.println("ok"); } } // main方法来测试 public static void main(String[] args) { Singleton instance = Singleton.INSTANCE; instance.sayOK(); } 优点:使用枚举,可以实现单例,避免了多线程同步问题,还能防止反序列化重新创建新的对象,推荐
补充Demo9:java核心类库 Runtime 的单例实现(饿汉式)
- 每个 Java 应用在运行时会启动一个 JVM 进程,每个 JVM 进程都只对应一个 Runtime 实例,用于查看 JVM 状态以及控制 JVM 行为。进程内唯一,所以比较适合设计为单例。
//静态实例被声明为final,一定程度上保证了实例不被篡改 public class Runtime { private Runtime(){} private static final Runtime currentRuntime = new Runtime(); private static Version version; public static Runtime getRuntime(){ return currentRuntime; } public void addShutdownHook(Thread hook) { SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new RuntimePermission("shutdownHooks")); } ApplicationShutdownHooks.add(hook); } }
Demo10: 获取 spi 单例
public class SpiProviderSelector { private static volatile SpiProviderSelector instance = null; private SpiProviderSelector(){} /* 获取单例*/ public static SpiProviderSelector getInstance() { if(instance == null){ synchronized (SpiProviderSelector.class){ if(instance == null){ instance = new SpiProviderSelector(); } } } return instance; } }
单例模式缺点:
1、单例对 OOP 特性的支持不友好
- 对继承、多态特性支持不友好
2、单例对代码的可测试性不友好
- 硬编码方式,无法实现 mock 替换
3、单例不支持有参数的构造函数
Demo11:怎么在单例模式中给对象初始化数据?
public class Singleton { private static Singleton instance = null; private final int paramA; private final int paramB; private Singleton(int paramA, int paramB) { this.paramA = paramA; this.paramB = paramB; } public static Singleton getInstance() { if (instance == null) { throw new RuntimeException("Run init() first."); } return instance; } public synchronized static Singleton init(int paramA, int paramB) { if (instance != null){ throw new RuntimeException("Singleton has been created!"); } instance = new Singleton(paramA, paramB); return instance; } } // 先init,再使用 getInstance() Singleton.init(10, 50); Singleton singleton = Singleton.getInstance()
单例模式的替代方案?
- 通过工厂模式、IOC 容器来保证全局唯一性。
Action2:请问 Spring下的 bean 单例模式与设计模式(GOF)中的单例模式区别?** 可以作为面试题
- 它们关联的环境不同,单例模式是指在一个JVM进程中仅有一个实例,不管在程序中的何处获取实例,始终都返回同一个对象。
Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。
Action3:单例模式实现方式总结
Action4:为什么说 Enum 实现单例模式是最佳实践
利用Enum实现单例模式
/* 使用枚举实现单例 */ public enum EnumSingleton { // 唯一的实例对象 INSTANCE; // 单例对象的属性对象 private Object obj = new Object(); public Object getObj() { return obj; } // 单例提供的对外服务。 public Object getFactoryService() { return new Object(); } }
反编译代码
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) // Source File Name: EnumSingleton.java public final class EnumSingleton extends Enum { public static EnumSingleton[] values() { return (EnumSingleton[])$VALUES.clone(); } public static EnumSingleton valueOf(String name) { return (EnumSingleton)Enum.valueOf(EnumSingleton, name); } // 无法通过new来随意创建对象,构造函数为private. private EnumSingleton(String s, int i) { super(s, i); obj = new Object(); } public Object getObj() { return obj; } public Object getFactoryService() { return new Object(); } // 提供获取唯一实例对象的方法,通常是getInstance // 也可以直接获取到INSTANCE,但是获取到的都是一个对象 public static final EnumSingleton INSTANCE; private Object obj; private static final EnumSingleton $VALUES[]; // 静态代码中实例化对象,多线程并发的情况下保证唯一,属于饿汉模式 static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); } }
从反编译代码中我们可以看到 Enum 的特点
- 枚举本质上是个final类
- 定义的枚举值实际上就是一个枚举类的不可变对象(比如这里的INSTANCE)
- 在Enum类加载的时候,就已经实例化了这个对象
- 无法通过 new 来创建枚举对象
Enum实现单例模式的几个关键点验证
1.避免反射创建单例对象(反射攻击)
枚举的私有构造函数如下所示:
private EnumSingleton(String s, int i)
尝试利用反射创建对象
/** * 反射攻击。 * 由于Enum天然的不允许反射创建实例,所以可以完美的防范反射攻击。 */ private static void reflectionAttack() { try { Constructor con = EnumSingleton.class.getDeclaredConstructor(String.class, int.class); con.setAccessible(true); // 反射新建对象以破坏单例 Object obj = con.newInstance("INSTANCE", 0); System.out.println(obj); System.out.println(EnumSingleton.getInstance()); } catch (Exception e) { e.printStackTrace(); } }
会抛出异常"java.lang.IllegalArgumentException: Cannot reflectively create enum objects"。如下所示:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:416) at com.ws.pattern.singleton.EnumSingletonAppMain.reflectionAttack(EnumSingletonAppMain.java:43) at com.ws.pattern.singleton.EnumSingletonAppMain.main(EnumSingletonAppMain.java:9)
从异常可以看出来,newInstance抛出了异常。推测Java反射是不允许创建Enum对象的,看看源码Constructor.java中的newInstance方法,存在处理Enum类型实例化的一行判断代码 if ((clazz.getModifiers() & Modifier.ENUM) != 0)
,满足这个条件就抛出异常。newInstance的JDK代码如下:
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); } } 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; }
2.避免通过序列化创建单例对象(如果单例类实现了Serializable)(序列化攻击)
- 序列化对象后,如果执行反序列化,也可以创建一个对象。利用此机制来尝试创建一个新的Enum对象。
/** 序列化攻击 * 需要在单例类中增加read */ private static void serializableAttack() { EnumSingleton singleton = EnumSingleton.getInstance(); System.out.println(singleton); try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./EnumSingleton.out")); oos.writeObject(singleton); ObjectInputStream ois = new ObjectInputStream((new FileInputStream("./EnumSingleton.out"))); Object obj = ois.readObject(); // 这里利用反序列化创建对象 System.out.println(obj); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
拿到了相同的对象。执行结果如下:
INSTANCE
INSTANCE
跟踪进入ois.readObject()
,会进入ObjectInputStream.readObject0方法。其中会解析class的二进制,根据class的文件定义,分别解析不同类型的字段。重点关注case TC_ENUM:
如下所示:
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: 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)); }
进入readEnum
方法,重点关注Enum.valueOf
方法。如下所示:
/** * Reads in and returns enum constant, or null if enum type is * unresolvable. Sets passHandle to enum constant's assigned handle. */ private Enum<?> readEnum(boolean unshared) throws IOException { if (bin.readByte() != TC_ENUM) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); if (!desc.isEnum()) { throw new InvalidClassException("non-enum class: " + desc); } int enumHandle = handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(enumHandle, resolveEx); } String name = readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") // 这里根据 name 和 class拿到 Enum 实例 Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } } handles.finish(enumHandle); passHandle = enumHandle; return result; }
再跟进Enum.valueOf
方法。代码如下:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { // 从enumConstantDirectory()中根据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); }
enumConstantDirectory()
是Class的方法,其本质是从Class.java的enumConstantDirectory
属性中获取。代码如下:
private volatile transient Map<String, T> enumConstantDirectory = null;
也就是说,Enum中定义的Enum成员值都被缓存在了这个Map中,Key是成员名称(比如“INSTANCE”),Value就是Enum的成员对象。这样的机制天然保证了取到的Enum对象是唯一的。即使是反序列化,也是一样的。
结论:单例枚举的实现简化了单例模式的创建,利用了枚举特性,使得单例枚举成为最佳实践。