1.单例模式
1.解题步骤
1.从定义入手
什么是单例模式?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。
在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
2.从类型入手
单例模式的类型
单例模式有两种类型:
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
- 懒汉式:在真正需要使用对象时才去创建该单例类对象
2.1饿汉式
- 是否懒加载 no 提前准备好的
- 线程安全 安全 static类 类加载就创建好
- 优点 没有锁,效率高
- 缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
- 描述 常用的方式
/*
饿汉式
1.是否懒加载 no 提前准备好的
2.线程安全 安全 static类 类加载就创建好
3.优点 没有锁,效率高
4.缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
5.描述 常用的方式
*/
public class HungrySingleton {
// 静态变量
private static final HungrySingleton instance = new HungrySingleton();
// 私有构造器
private HungrySingleton() {}
// 提供方法
public static HungrySingleton getInstance() {
return instance;
}
// 公有方法
public void showMessage() {
System.out.println("HungrySingleton");
}
}
2.2懒汉式
1.是否懒加载 yes 要用才创建
2.线程安全 否
3.描述 常用的懒汉创建方式,最大的问题是不支持多线程。本质上不是单例模式
每次获取对象都要获取锁,性能差
/*
饿汉式
1.是否懒加载 yes 要用才创建
2.线程安全 否
3.描述 常用的懒汉创建方式,最大的问题是不支持多线程。本质上不是单例模式
volatile 特征修饰符(type specifier) 和底层指令有关 排序性
确保本条语句不会被编译器优化,防止指令重排序
*/
public class LazySingleton {
// 静态变量
private static volatile LazySingleton instance;
// 私有构造器
private LazySingleton() {}
// 提供方法
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
// 公有方法
public void showMessage() {
System.out.println("LazySingleton");
}
}
2.3 LazySynchronizedSingleton
/*
饿汉式
1.是否懒加载 yes 要用才创建
2.线程安全 是 synchronized
3.优点 调用一次就可以完成对象的创建 单例
4.缺点 执行效率低,其他线程容易阻塞 线程锁修饰 加锁影响效率
5.描述 常用的线程安全懒汉创建方式
*/
public class LazySynchronizedSingleton {
// 静态变量
private static LazySynchronizedSingleton instance;
// 私有构造器
private LazySynchronizedSingleton() {}
// 提供方法
public static synchronized LazySynchronizedSingleton getInstance() {
if (instance == null) {
instance = new LazySynchronizedSingleton();
}
return instance;
}
// 公有方法
public void showMessage() {
System.out.println("LazySingleton");
}
}
2.4 DCLSingleton双检锁
两次null判断
第一次是 避免直接进入锁,如果没有,全部都获取锁很浪费
第二次是可能多个线程同时进入锁,如果不进行判断可能会创建多个对象
/*
DCL double checked locking 双检锁 volatile synchronized
1.是否懒加载 yes 要用才创建
2.线程安全 是 synchronized
3.优点 调用一次就可以完成对象的创建 单例
4.缺点 执行效率低,其他线程容易阻塞 线程锁修饰 加锁影响效率
5.描述 常用的线程安全懒汉创建方式
*/
public class DCLSingleton {
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
public void show(){
System.out.println("DCLSingleton");
}
}
使用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)
使用volatile关键字可以防止指令重排序,可以这样理解:使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。NPE(null point exception)
volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量
破坏懒汉式单例与饿汉式单例
无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。
1.演示利用反射破坏单例模式
public static void main(String[] args) {
// 获取类的显式构造器
Constructor construct = Singleton.class.getDeclaredConstructor();
// 可访问私有构造器
construct.setAccessible(true);
// 利用反射构造新对象
Singleton obj1 = construct.newInstance();
// 通过正常方式获取单例对象
Singleton obj2 = Singleton.getInstance();
System.out.println(obj1 == obj2); // false
}
上述的代码一针见血了:利用反射,强制访问类的私有构造器,去创建另一个对象
2.利用序列化与反序列化破坏单例模式
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("Singleton.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == Singleton.getInstance()); // false
}
两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址。
2.5 登记式
/*
登记式
1.是否懒加载 no 提前准备好的
2.线程安全 安全 static类 类加载就创建好
3.优点 没有锁,效率高
4.缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
5.描述 常用的方式
*/
public class RegisterSingleton {
private RegisterSingleton() {}
public static final RegisterSingleton getInstance() {
return SingletonHolder.instance;
}
public void show() {
System.out.println("RegisterSingleton");
}
// 类static
private static class SingletonHolder {
// 定义一个静态的常量内部对象
private static final RegisterSingleton instance = new RegisterSingleton();
}
}
2.6 枚举式
/*
单例模式采用枚举类
*/
/*
枚举式
1.是否懒加载 no 提前准备好的
2.线程安全 安全
3.优点 没有锁,效率高
4.缺点 加载过早,浪费内存,外界没有访问,就可能变成垃圾对象
5.描述 单例最为推荐的方式
*/
public enum EnumSingleton {
instance;
public void enumMethod() {
System.out.println("枚举式创建");
}
}
需要思考:使用枚举实现单例模式的优势在哪里?
我们从最直观的地方入手,第一眼看到这几行代码,就会感觉到“少”,没错,就是少,虽然这优势有些牵强,但写的代码越少,越不容易出错。
优势1:代码对比饿汉式与懒汉式来说,更加地简洁
其次,既然是实现单例模式,那这种写法必定满足单例模式的要求,而且使用枚举实现时,没有做任何额外的处理。
优势2:它不需要做任何额外的操作去保证对象单一性与线程安全性
我写了一段测试代码放在下面,这一段代码可以证明程序启动时仅会创建一个 Singleton 对象,且是线程安全的。
我们可以简单地理解枚举实现单例的过程:在程序启动时,会调用Singleton的空参构造器,实例化好一个Singleton对象赋给INSTANCE,之后再也不会实例化
public enum Singleton {
INSTANCE;
Singleton() {
System.out.println("枚举创建对象了");
}
public static void main(String[] args) {
test();
}
public void test() {
Singleton t1 = Singleton.INSTANCE;
Singleton t2 = Singleton.INSTANCE;
System.out.print("t1和t2的地址是否相同:" + t1 == t2);
}
}
// 枚举创建对象了
// t1和t2的地址是否相同:true
除了优势1和优势2,还有最后一个优势让枚举实现单例模式在目前看来已经是“无懈可击”了。
优势3:使用枚举可以防止调用者使用反射、序列化与反序列化机制强制生成多个单例对象,破坏单例模式。
防破坏的原理如下:
(1)防反射
枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
(2)防止反序列化创建多个枚举对象
在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
枚举式小总结
(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.接口
public interface Rent {
void rent();
}
2.真实对象
public class Host implements Rent {
@Override
public void rent() {
System.out.println("房东要租房子!!!!");
}
}
3.代理对象
public class Proxy implements Rent {
private Host host;
public Proxy() {}
public Proxy(Host host) {
this.host = host;
}
@Override
public void rent() {
host.rent();
// 中介还可以增加其他操作,
seeHouse();
fare();
hetong();
}
//
public void seeHouse() {
System.out.println("中介带你看房");
}
public void fare() {
System.out.println("中介收费");
}
public void hetong() {
System.out.println("中介签合同");
}
}
4.客户
public class Client {
public static void main(String[] args) {
// 直接找房东租房,但是现实找不到房东,找中介
// Host host = new Host();
// host.rent();
Host host = new Host();
Proxy proxy = new Proxy(host);
proxy.rent();
}
2.动态代理
1.角色分析
1.动态代理和静态代理角色一样
2.动态代理的代理类是动态生成的,不是我们自定义的
3.分为两大类
1.jdk动态代理 基于接口的
2.基于类的 CGLIB
3.java字节码 JBOSS
2.好处
静态代理的所有好处
一个动态代理类可以代理多个类,只要实现了接口
public class ProxyInvocationHandler implements InvocationHandler {
// 被代理的接口
private Rent rent;
public void setRent(Rent rent) {
this.rent = rent;
}
// 生成得到代理类
public Object getProxy() {
return Proxy.newProxyInstance(
rent.getClass().getClassLoader(), rent.getClass().getInterfaces(), this);
}
// 处理代理实例,返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 动态代理的本质,通过反射机制实现
Object result = method.invoke(rent, args);
return result;
}
}
4.模板模式
1.关键点
模板方法模式是一个比较实用且简单的设计模式,它的关键点有:
(1)父类提供好方法模板,交给子类去实现
(2)父类定义好方法的执行顺序,对外暴露出一个执行方法,让子类调用执行。
2.什么是模板方法模式
举一个贴近生活的例子:我们每个人的早上、下午、晚上都可以做不同的事:
(1)男程序员:早上敲代码、下午摸鱼、晚上多人运动
(2)女程序员:早上敲代码、下午Shopping、晚上看剧吃沙拉美容
(3)···
我们可以发现,不同的人有不同的行为,而每个人的行为都是有相同的执行顺序的(早上、下午、晚上)
我们回顾上面的两位成员,用代码表示是这样的:
public class GeGe {
public void morning() {
System.out.println("男程序员在敲代码");
}
public void afternoon() {
System.out.println("男程序员在摸鱼");
}
public void evening() {
System.out.println("男程序员在多人运动");
}
public void start() {
morning();
afternoon();
evening();
}
}
public class MeiMei {
public void morning() {
System.out.println("女程序员在敲代码");
}
public void afternoon() {
System.out.println("女程序员在Shopping");
}
public void evening() {
System.out.println("女程序员在看剧吃沙拉和护肤,美美哒");
}
public void start() {
morning();
afternoon();
evening();
}
}
如果还有一个DiDi(弟弟)类,他也要过每一天的生活,那样在程序里还要重新手写这几个方法,再实现它的逻辑,太龊了,一点都不程序员,所以我们很快可以想到:把相同的方法抽取出来,聚合到一个抽象类Human当中,让GeGe和MeiMei去继承这个抽象类,实现抽象方法,这样就优雅多了吧
public abstract class Human {
protected abstract void morning();
protected abstract void afternoon();
protected abstract void evening();
protected void start() {
morning();
afternoon();
evening();
}
}
到这里,你会看到 Human 就像一个模板类一样,提供好了三个方法让子类去重写,并且提供了调用方法。GeGe类就变成这样了:
public class GeGe extends Human {
@Override
public void morning() {
System.out.println("男程序员在敲代码");
}
@Override
public void afternoon() {
System.out.println("男程序员在摸鱼");
}
@Override
public void evening() {
System.out.println("男程序员在多人运动");
}
}
public static void main(String[] args) {
Human gege = new GeGe();
gege.start();
//// 男程序员在敲代码
// 男程序员在摸鱼
// 男程序员在多人运动
}
同理,MeiMei 类就不贴出来了。这样的好处是:
(1)Human(人类)类已经提供好需要实现的方法,GeGe(哥哥)不需要去思考自己需要写哪些方法,直接重写父类要求的方法即可。
(2)由于在父类中已经提供了方法,里面定义好每个方法的执行顺序,子类按照父类给定的调用顺序去编写逻辑即可。
3.使用 final 关键字
我们来回顾一下 Human 这一段代码
public abstract class Human {
protected abstract void morning();
protected abstract void afternoon();
protected abstract void evening();
protected void start() {
morning();
afternoon();
evening();
}
}
上面这段代码已经可以称为一个“合格”的模板方法模式了,但是还差一步,那就是标题中的“final”关键字。
final 关键字有以下作用:
(1)使一个方法永远无法被重写(重要!)
(2)使一个变量的值永远不能发生改变(引用变量则代表引用地址的值)
(3)还有其它作用,但不是这篇博客的重点,可以参考 final关键字的所有作用
在模板方法模式下,子类不能破坏父类的调用顺序,所以为了保证这一点,我们需要在方法上加上 final 关键字。
public final void start() {
morning();
afternoon();
evening();
}
小小的总结
到这里为止,我对模板方法模式的关键点做了小小的总结:
(1)模板方法模式是一个由父类提供好抽象方法以及调用顺序、由子类实现具体逻辑的设计模式
(2)使用 final 关键字的作用是确保子类无法破坏父类定义好的调用顺序,否则违背了该模式的定义(可以对final关键字的作用展开哦)
4.扩展使用:钩子方法
钩子方法,第一次听到这个名字的时候,我的表情是这样的,为什么叫钩子,钩啥玩意儿?
后来搜了一下它的定义,简单理解,钩子方法:管理着某个方法,决定该方法是否执行的一个管理者
文字较为晦涩,我们直接来看下面这段代码:
public abstract class Human {
protected abstract void morning();
protected abstract void afternoon();
protected abstract void evening();
protected void start() {
if (doSthOnMorning()) {
morning();
}
afternoon();
evening();
}
protected boolean doSthOnMorning() {
return false;
}
}
上面这段代码中的 doSthOnMorning 方法就是钩子方法:它决定了 morning 方法是否执行。如代码所示:如果早上没事做,就不需要调用 morning() 方法去告诉别人自己早上做了什么了。
我们可以这样理解钩子方法:它就是一个业务方法的开关按钮。
如果按钮打开(true),那么业务方法就会被执行
否则按钮关闭(false),业务方法不会被执行
5. 模板方法模式总结
父类的作用
(1)父类总结好一套抽象方法(算法)模板,并对外暴露出一个通用的、不可被重写的执行方法,其内部定义好模板的调用顺序,让子类去调用。
(2)父类提供好每个算法模板的钩子(hook)方法,让子类自行决定是否要执行对应的方法
子类的作用
(1)子类实现父类提供的抽象方法模板和钩子函数,自行实现每个抽象方法模板的逻辑
(2)子类通过操控钩子函数决定是否执行子类中实现好的方法模板
可以围绕 final 关键字进行知识点的拓展进行讲解,所有面试官都喜欢主动的面试者!
[
](https://blog.csdn.net/weixin_41949328/article/details/107374913)