目录
Java 中一般认为有 23 种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
关于单例模式(Singleton)
单例模式(Singleton Pattern) - 保证一个类仅有一个对象,并提供一个它的全局访问点
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
常见应用场景:
- Windows的Task Manager(任务管理器)就是很典型的单例模式
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作否则内容不好追加。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- Application 也是单例的典型应用(Servlet编程中会涉及到)
- 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
- 在servlet编程中,每个Servlet也是单例
- 在spring MVC框架/struts1框架中,控制器对象也是单例
- 一个产品注册了一个商标,那么它就是单例的
单例模式的优点:
- 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动 时直接产生一个单例对象,然后永久驻留内存的方式来解决
- 单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计 一个单例类,负责所有数据表的映射处理
单例模式的类型(待补充):
- 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
- 懒汉式:在真正需要使用对象时才去创建该单例类对象
- 枚举式:枚举本身就是单例模式,线程安全,实现简单,调用效率高,不能延时加载。
一、饿汉式(Hungry)
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
public class hungry {
// jvm保证在任何线程访问uniqueInstance静态变量之前一定先创建了此实例
private final static hungry HUNGRY = new hungry();
// 私有构造器,外界无法实例化
private hungry(){}
// 提供全局访问点获取唯一的实例
public hungry getHungry(){
return HUNGRY;
}
}
- 存在的问题:可能会浪费内存空间
二、懒汉式(LazyMan)
懒汉式--在使用时候才去实例化单例对象
public class lazyMan {
private lazyMan(){}
private static lazyMan LazyMan;
public static lazyMan getLazyMan(){
//对象唯一
if(LazyMan==null){
LazyMan = new lazyMan();
}
return LazyMan;
}
}
- 安全问题
但由于创建对象并非原子性操作,多线程并发可能会创建多个对象,我们需要进一步优化
2、DCL懒汉式 -- 双重检测锁模式
- DCL:Double Check(双重校验) + Lock(加锁)
public static lazyMan getLazyMan(){
//多线程同时看到LazyMan==null,如果不为null,则直接返回LazyMan
if(LazyMan==null){
synchronized (lazyMan.class){
if(LazyMan==null){
//其中一个线程进入,另一个则检测已不为null
LazyMan = new lazyMan(); //不是原子性操作
/**
* 1、分配内存空间
* 2、执行方法构造,初始化内存
* 3、把这个对象指向这个空间
* 1,3,2 A
* 若此时 B 进入,LazyMan还没完成构造,代码仍存在问题
*/
}
}
}
return LazyMan;
}
但由于创建对象并非原子性操作,以上代码还存在问题,指令重排。
3、使用volatile防止指令重排
- 指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能
创建对象时,在JVM会经过三步:
(1)分配内存空间
(2)执行方法构造,初始化内存
(3)将对象指向分配好的这个空间
如果A线程创建对象的步骤为1、3、2,B线程在执行完1、3后进入,判定到LazyMan已不为空,return 获取到未初始化的LazyMan对象,则会造成空指针异常。
- volatile
使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换。
volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
- 最终代码:
public class lazyMan {
private lazyMan(){}
private static volatile lazyMan LazyMan; //volatile防止指令重排
public static lazyMan getLazyMan(){
if(LazyMan==null){
synchronized (lazyMan.class){
if(LazyMan==null){
LazyMan = new lazyMan();
}
}
}
return LazyMan;
}
}
4、通过反射、序列化破坏单例模式
但由于反射和序列化的存在,他们依然可以将单例对象破坏(产生多个对象),造成安全问题。
1:演示利用反射破坏单例模式
public static void main(String[] args) throws Exception {
// 获取类的显式构造器
Constructor<lazyMan> lazyManConstructor = lazyMan.class.getDeclaredConstructor();
// 可访问私有构造器
lazyManConstructor.setAccessible(true);
// 利用反射构造新对象
lazyMan lazyMan = lazyManConstructor.newInstance();
lazyMan lazyMan2 =lazyManConstructor.newInstance();
System.out.println(lazyMan == lazyMan2); //false
}
2:利用序列化与反序列化破坏单例模式
public static void main(String[] args) {
// 创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("lazyMan.file"));
// 将单例对象写到文件中
oos.writeObject(Singleton.getInstance());
// 从文件中读取单例对象
File file = new File("lazyMan.file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
lazyMan newInstance = (lazyMan) ois.readObject();
// 判断是否是同一个对象
System.out.println(newInstance == lazyMan.getInstance()); // false
}
三、枚举式(EnumSingle)
在 JDK1.5 后,使用 Java 语言实现单例模式的方式又多了一种:枚举
// enum 本身也是一个Class类
public enum EnumSingle {
//定义一个枚举的元素,它就代表了Singleton的一个实例。
INSTANCE;
//对外部提供调用方法:将创建的对象返回,只能通过类来调用
public void otherMethod(){
//功能处理
}
public static void main(String[] args) {
EnumSingle i1 = EnumSingle.INSTANCE;
EnumSingle i2 = EnumSingle.INSTANCE;
System.out.println(i1==i2); //true
}
}
(1)防反射:在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。newInstance()源码:
- Cannot reflectively create enum objects - 不能反射地创建enum对象
(2)防止反序列化: 在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
小总结:
(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象
(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象
(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。
————————————————
参考:https://blog.csdn.net/weixin_41949328/article/details/107296517