1.单例模式

1.解题步骤

1.从定义入手

什么是单例模式?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。

在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
image.png

2.从类型入手

单例模式的类型
单例模式有两种类型:

  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
  • 懒汉式:在真正需要使用对象时才去创建该单例类对象

2.1饿汉式

  1. 是否懒加载 no 提前准备好的
  2. 线程安全 安全 static类 类加载就创建好
  3. 优点 没有锁,效率高
  4. 缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
  5. 描述 常用的方式
  1. /*
  2. 饿汉式
  3. 1.是否懒加载 no 提前准备好的
  4. 2.线程安全 安全 static类 类加载就创建好
  5. 3.优点 没有锁,效率高
  6. 4.缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
  7. 5.描述 常用的方式
  8. */
  9. public class HungrySingleton {
  10. // 静态变量
  11. private static final HungrySingleton instance = new HungrySingleton();
  12. // 私有构造器
  13. private HungrySingleton() {}
  14. // 提供方法
  15. public static HungrySingleton getInstance() {
  16. return instance;
  17. }
  18. // 公有方法
  19. public void showMessage() {
  20. System.out.println("HungrySingleton");
  21. }
  22. }

2.2懒汉式

1.是否懒加载 yes 要用才创建
2.线程安全 否
3.描述 常用的懒汉创建方式,最大的问题是不支持多线程。本质上不是单例模式
每次获取对象都要获取锁,性能差

  1. /*
  2. 饿汉式
  3. 1.是否懒加载 yes 要用才创建
  4. 2.线程安全 否
  5. 3.描述 常用的懒汉创建方式,最大的问题是不支持多线程。本质上不是单例模式
  6. volatile 特征修饰符(type specifier) 和底层指令有关 排序性
  7. 确保本条语句不会被编译器优化,防止指令重排序
  8. */
  9. public class LazySingleton {
  10. // 静态变量
  11. private static volatile LazySingleton instance;
  12. // 私有构造器
  13. private LazySingleton() {}
  14. // 提供方法
  15. public static LazySingleton getInstance() {
  16. if (instance == null) {
  17. instance = new LazySingleton();
  18. }
  19. return instance;
  20. }
  21. // 公有方法
  22. public void showMessage() {
  23. System.out.println("LazySingleton");
  24. }
  25. }

2.3 LazySynchronizedSingleton

  1. /*
  2. 饿汉式
  3. 1.是否懒加载 yes 要用才创建
  4. 2.线程安全 是 synchronized
  5. 3.优点 调用一次就可以完成对象的创建 单例
  6. 4.缺点 执行效率低,其他线程容易阻塞 线程锁修饰 加锁影响效率
  7. 5.描述 常用的线程安全懒汉创建方式
  8. */
  9. public class LazySynchronizedSingleton {
  10. // 静态变量
  11. private static LazySynchronizedSingleton instance;
  12. // 私有构造器
  13. private LazySynchronizedSingleton() {}
  14. // 提供方法
  15. public static synchronized LazySynchronizedSingleton getInstance() {
  16. if (instance == null) {
  17. instance = new LazySynchronizedSingleton();
  18. }
  19. return instance;
  20. }
  21. // 公有方法
  22. public void showMessage() {
  23. System.out.println("LazySingleton");
  24. }
  25. }

2.4 DCLSingleton双检锁

两次null判断
第一次是 避免直接进入锁,如果没有,全部都获取锁很浪费
第二次是可能多个线程同时进入锁,如果不进行判断可能会创建多个对象

  1. /*
  2. DCL double checked locking 双检锁 volatile synchronized
  3. 1.是否懒加载 yes 要用才创建
  4. 2.线程安全 是 synchronized
  5. 3.优点 调用一次就可以完成对象的创建 单例
  6. 4.缺点 执行效率低,其他线程容易阻塞 线程锁修饰 加锁影响效率
  7. 5.描述 常用的线程安全懒汉创建方式
  8. */
  9. public class DCLSingleton {
  10. private static volatile DCLSingleton instance;
  11. private DCLSingleton() {}
  12. public static DCLSingleton getInstance() {
  13. if (instance == null) {
  14. synchronized (DCLSingleton.class) {
  15. if (instance == null) {
  16. instance = new DCLSingleton();
  17. }
  18. }
  19. }
  20. return instance;
  21. }
  22. public void show(){
  23. System.out.println("DCLSingleton");
  24. }
  25. }

使用volatile防止指令重排

创建一个对象,在JVM中会经过三步:

(1)为singleton分配内存空间

(2)初始化singleton对象

(3)将singleton指向分配好的内存空间

指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。文字较为晦涩,可以看流程图:
[

](https://blog.csdn.net/weixin_41949328/article/details/107296517)

image.png

使用volatile关键字可以防止指令重排序,可以这样理解:使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。NPE(null point exception)

volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量

破坏懒汉式单例与饿汉式单例


无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。

1.演示利用反射破坏单例模式
  1. public static void main(String[] args) {
  2. // 获取类的显式构造器
  3. Constructor construct = Singleton.class.getDeclaredConstructor();
  4. // 可访问私有构造器
  5. construct.setAccessible(true);
  6. // 利用反射构造新对象
  7. Singleton obj1 = construct.newInstance();
  8. // 通过正常方式获取单例对象
  9. Singleton obj2 = Singleton.getInstance();
  10. System.out.println(obj1 == obj2); // false
  11. }


上述的代码一针见血了:利用反射,强制访问类的私有构造器,去创建另一个对象

2.利用序列化与反序列化破坏单例模式
  1. public static void main(String[] args) {
  2. // 创建输出流
  3. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
  4. // 将单例对象写到文件中
  5. oos.writeObject(Singleton.getInstance());
  6. // 从文件中读取单例对象
  7. File file = new File("Singleton.file");
  8. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  9. Singleton newInstance = (Singleton) ois.readObject();
  10. // 判断是否是同一个对象
  11. System.out.println(newInstance == Singleton.getInstance()); // false
  12. }


两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址。

2.5 登记式

  1. /*
  2. 登记式
  3. 1.是否懒加载 no 提前准备好的
  4. 2.线程安全 安全 static类 类加载就创建好
  5. 3.优点 没有锁,效率高
  6. 4.缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
  7. 5.描述 常用的方式
  8. */
  9. public class RegisterSingleton {
  10. private RegisterSingleton() {}
  11. public static final RegisterSingleton getInstance() {
  12. return SingletonHolder.instance;
  13. }
  14. public void show() {
  15. System.out.println("RegisterSingleton");
  16. }
  17. // 类static
  18. private static class SingletonHolder {
  19. // 定义一个静态的常量内部对象
  20. private static final RegisterSingleton instance = new RegisterSingleton();
  21. }
  22. }

2.6 枚举式

  1. /*
  2. 单例模式采用枚举类
  3. */
  4. /*
  5. 枚举式
  6. 1.是否懒加载 no 提前准备好的
  7. 2.线程安全 安全
  8. 3.优点 没有锁,效率高
  9. 4.缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
  10. 5.描述 单例最为推荐的方式
  11. */
  12. public enum EnumSingleton {
  13. instance;
  14. public void enumMethod() {
  15. System.out.println("枚举式创建");
  16. }
  17. }

需要思考:使用枚举实现单例模式的优势在哪里?

我们从最直观的地方入手,第一眼看到这几行代码,就会感觉到“少”,没错,就是少,虽然这优势有些牵强,但写的代码越少,越不容易出错。

优势1:代码对比饿汉式与懒汉式来说,更加地简洁

其次,既然是实现单例模式,那这种写法必定满足单例模式的要求,而且使用枚举实现时,没有做任何额外的处理。

优势2:它不需要做任何额外的操作去保证对象单一性与线程安全性

我写了一段测试代码放在下面,这一段代码可以证明程序启动时仅会创建一个 Singleton 对象,且是线程安全的。

我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化

  1. public enum Singleton {
  2. INSTANCE;
  3. Singleton() {
  4. System.out.println("枚举创建对象了");
  5. }
  6. public static void main(String[] args) {
  7. test();
  8. }
  9. public void test() {
  10. Singleton t1 = Singleton.INSTANCE;
  11. Singleton t2 = Singleton.INSTANCE;
  12. System.out.print("t1和t2的地址是否相同:" + t1 == t2);
  13. }
  14. }
  15. // 枚举创建对象了
  16. // t1和t2的地址是否相同:true

除了优势1和优势2,还有最后一个优势让枚举实现单例模式在目前看来已经是“无懈可击”了。

优势3:使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。

防破坏的原理如下:

(1)防反射
image.png

枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。

(2)防止反序列化创建多个枚举对象

在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。

所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
image.png

枚举式小总结

(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象

(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象

(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。
[

](https://blog.csdn.net/weixin_41949328/article/details/107296517)

3.总结

(1)单例模式常见的写法有两种:懒汉式、饿汉式

(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题

(6)为了防止多线程环境下,因为指令重排序导致变量报NPE,需要在单例对象上添加volatile关键字防止指令重排序

(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。
[

](https://blog.csdn.net/weixin_41949328/article/details/107296517)

2.工厂模式

创建型

1.概念

工厂模式的主要解决的问题是,将原来分布在各个地方的对象创建过程单独抽离出来,交给工厂类负责创建。其他地方想要使用对象直接找工厂(即调用工厂的方法)获取对象

2.分类

1.简单工厂

2.工厂方法

3.抽象工厂

3.代理模式

结构型

1.静态代理

1.角色分析

1.抽象角色 一般使用接口或者抽象类来解决
2.真实角色 被代理的对象
3.代理角色 代理真实角色,代理真实角色后,一般会增加一些附属操作,不然代理就没意义了
4.客户 访问代理对象的人

2.好处

1.可以让真实角色的操作更加纯粹,不用关注一些公共的业务
2.公共业务也交给代理角色,实现了业务的分工
3.公共业务发生扩展的时候,方便集中管理
耦合性降低

3.缺点

一个真实角色就会产生一个代理,开发效率会低

4.步骤

1.接口
  1. public interface Rent {
  2. void rent();
  3. }

2.真实对象
  1. public class Host implements Rent {
  2. @Override
  3. public void rent() {
  4. System.out.println("房东要租房子!!!!");
  5. }
  6. }

3.代理对象
  1. public class Proxy implements Rent {
  2. private Host host;
  3. public Proxy() {}
  4. public Proxy(Host host) {
  5. this.host = host;
  6. }
  7. @Override
  8. public void rent() {
  9. host.rent();
  10. // 中介还可以增加其他操作,
  11. seeHouse();
  12. fare();
  13. hetong();
  14. }
  15. //
  16. public void seeHouse() {
  17. System.out.println("中介带你看房");
  18. }
  19. public void fare() {
  20. System.out.println("中介收费");
  21. }
  22. public void hetong() {
  23. System.out.println("中介签合同");
  24. }
  25. }

4.客户
  1. public class Client {
  2. public static void main(String[] args) {
  3. // 直接找房东租房,但是现实找不到房东,找中介
  4. // Host host = new Host();
  5. // host.rent();
  6. Host host = new Host();
  7. Proxy proxy = new Proxy(host);
  8. proxy.rent();
  9. }

2.动态代理

1.角色分析

1.动态代理和静态代理角色一样
2.动态代理的代理类是动态生成的,不是我们自定义的
3.分为两大类
1.jdk动态代理 基于接口的
2.基于类的 CGLIB
3.java字节码 JBOSS

2.好处

静态代理的所有好处
一个动态代理类可以代理多个类,只要实现了接口

  1. public class ProxyInvocationHandler implements InvocationHandler {
  2. // 被代理的接口
  3. private Rent rent;
  4. public void setRent(Rent rent) {
  5. this.rent = rent;
  6. }
  7. // 生成得到代理类
  8. public Object getProxy() {
  9. return Proxy.newProxyInstance(
  10. rent.getClass().getClassLoader(), rent.getClass().getInterfaces(), this);
  11. }
  12. // 处理代理实例,返回结果
  13. @Override
  14. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  15. // 动态代理的本质,通过反射机制实现
  16. Object result = method.invoke(rent, args);
  17. return result;
  18. }
  19. }

4.模板模式

行为型

1.关键点

模板方法模式是一个比较实用且简单的设计模式,它的关键点有:

(1)父类提供好方法模板,交给子类去实现

(2)父类定义好方法的执行顺序,对外暴露出一个执行方法,让子类调用执行。

2.什么是模板方法模式


举一个贴近生活的例子:我们每个人的早上、下午、晚上都可以做不同的事:

(1)男程序员:早上敲代码、下午摸鱼、晚上多人运动

(2)女程序员:早上敲代码、下午Shopping、晚上看剧吃沙拉美容

(3)···

我们可以发现,不同的人有不同的行为,而每个人的行为都是有相同的执行顺序的(早上、下午、晚上)

我们回顾上面的两位成员,用代码表示是这样的:

  1. public class GeGe {
  2. public void morning() {
  3. System.out.println("男程序员在敲代码");
  4. }
  5. public void afternoon() {
  6. System.out.println("男程序员在摸鱼");
  7. }
  8. public void evening() {
  9. System.out.println("男程序员在多人运动");
  10. }
  11. public void start() {
  12. morning();
  13. afternoon();
  14. evening();
  15. }
  16. }
  1. public class MeiMei {
  2. public void morning() {
  3. System.out.println("女程序员在敲代码");
  4. }
  5. public void afternoon() {
  6. System.out.println("女程序员在Shopping");
  7. }
  8. public void evening() {
  9. System.out.println("女程序员在看剧吃沙拉和护肤,美美哒");
  10. }
  11. public void start() {
  12. morning();
  13. afternoon();
  14. evening();
  15. }
  16. }


如果还有一个DiDi(弟弟)类,他也要过每一天的生活,那样在程序里还要重新手写这几个方法,再实现它的逻辑,太龊了,一点都不程序员,所以我们很快可以想到:把相同的方法抽取出来,聚合到一个抽象类Human当中,让GeGe和MeiMei去继承这个抽象类,实现抽象方法,这样就优雅多了吧

  1. public abstract class Human {
  2. protected abstract void morning();
  3. protected abstract void afternoon();
  4. protected abstract void evening();
  5. protected void start() {
  6. morning();
  7. afternoon();
  8. evening();
  9. }

}
到这里,你会看到 Human 就像一个模板类一样,提供好了三个方法让子类去重写,并且提供了调用方法。GeGe类就变成这样了:

  1. public class GeGe extends Human {
  2. @Override
  3. public void morning() {
  4. System.out.println("男程序员在敲代码");
  5. }
  6. @Override
  7. public void afternoon() {
  8. System.out.println("男程序员在摸鱼");
  9. }
  10. @Override
  11. public void evening() {
  12. System.out.println("男程序员在多人运动");
  13. }
  14. }
  1. public static void main(String[] args) {
  2. Human gege = new GeGe();
  3. gege.start();
  4. //// 男程序员在敲代码
  5. // 男程序员在摸鱼
  6. // 男程序员在多人运动
  7. }


同理,MeiMei 类就不贴出来了。这样的好处是:

(1)Human(人类)类已经提供好需要实现的方法,GeGe(哥哥)不需要去思考自己需要写哪些方法,直接重写父类要求的方法即可。

(2)由于在父类中已经提供了方法,里面定义好每个方法的执行顺序,子类按照父类给定的调用顺序去编写逻辑即可。

3.使用 final 关键字


我们来回顾一下 Human 这一段代码

  1. public abstract class Human {
  2. protected abstract void morning();
  3. protected abstract void afternoon();
  4. protected abstract void evening();
  5. protected void start() {
  6. morning();
  7. afternoon();
  8. evening();
  9. }
  10. }


上面这段代码已经可以称为一个“合格”的模板方法模式了,但是还差一步,那就是标题中的“final”关键字。

final 关键字有以下作用:

(1)使一个方法永远无法被重写(重要!)

(2)使一个变量的值永远不能发生改变(引用变量则代表引用地址的值)

(3)还有其它作用,但不是这篇博客的重点,可以参考 final关键字的所有作用

在模板方法模式下,子类不能破坏父类的调用顺序,所以为了保证这一点,我们需要在方法上加上 final 关键字。

  1. public final void start() {
  2. morning();
  3. afternoon();
  4. evening();
  5. }

小小的总结


到这里为止,我对模板方法模式的关键点做了小小的总结:

(1)模板方法模式是一个由父类提供好抽象方法以及调用顺序、由子类实现具体逻辑的设计模式

(2)使用 final 关键字的作用是确保子类无法破坏父类定义好的调用顺序,否则违背了该模式的定义(可以对final关键字的作用展开哦)

4.扩展使用:钩子方法


钩子方法,第一次听到这个名字的时候,我的表情是这样的,为什么叫钩子,钩啥玩意儿?

后来搜了一下它的定义,简单理解,钩子方法:管理着某个方法,决定该方法是否执行的一个管理者

文字较为晦涩,我们直接来看下面这段代码:

  1. public abstract class Human {
  2. protected abstract void morning();
  3. protected abstract void afternoon();
  4. protected abstract void evening();
  5. protected void start() {
  6. if (doSthOnMorning()) {
  7. morning();
  8. }
  9. afternoon();
  10. evening();
  11. }
  12. protected boolean doSthOnMorning() {
  13. return false;
  14. }

}
上面这段代码中的 doSthOnMorning 方法就是钩子方法:它决定了 morning 方法是否执行。如代码所示:如果早上没事做,就不需要调用 morning() 方法去告诉别人自己早上做了什么了。

我们可以这样理解钩子方法:它就是一个业务方法的开关按钮。

如果按钮打开(true),那么业务方法就会被执行
否则按钮关闭(false),业务方法不会被执行


5. 模板方法模式总结


父类的作用
(1)父类总结好一套抽象方法(算法)模板,并对外暴露出一个通用的、不可被重写的执行方法,其内部定义好模板的调用顺序,让子类去调用。

(2)父类提供好每个算法模板的钩子(hook)方法,让子类自行决定是否要执行对应的方法

子类的作用
(1)子类实现父类提供的抽象方法模板和钩子函数,自行实现每个抽象方法模板的逻辑

(2)子类通过操控钩子函数决定是否执行子类中实现好的方法模板

可以围绕 final 关键字进行知识点的拓展进行讲解,所有面试官都喜欢主动的面试者!

[

](https://blog.csdn.net/weixin_41949328/article/details/107374913)