目录
关于单例模式(Singleton)
饿汉式(Hungry)
1、懒汉式(LazyMan)
2、DCL懒汉式 -- 双重检测锁模式
3、使用volatile防止指令重排
4、通过反射、序列化破坏单例模式
枚举式(EnumSingle)
Java 中一般认为有 23 种设计模式,我们不需要所有的都会,但是其中常用的几种设计模式应该去掌握。总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
单例模式(Singleton Pattern) - 保证一个类仅有一个对象,并提供一个它的全局访问点
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
常见应用场景:
- Windows的Task Manager(任务管理器)就是很典型的单例模式
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作否则内容不好追加。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- Application 也是单例的典型应用(Servlet编程中会涉及到)
- 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
- 在servlet编程中,每个Servlet也是单例
- 在spring MVC框架/struts1框架中,控制器对象也是单例
- 一个产品注册了一个商标,那么它就是单例的
单例模式的优点:
单例模式的类型(待补充):
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
public class hungry {
// jvm保证在任何线程访问uniqueInstance静态变量之前一定先创建了此实例
private final static hungry HUNGRY = new hungry();
// 私有构造器,外界无法实例化
private hungry(){}
// 提供全局访问点获取唯一的实例
public hungry getHungry(){
return HUNGRY;
}
}
懒汉式--在使用时候才去实例化单例对象
public class lazyMan {
private lazyMan(){}
private static lazyMan LazyMan;
public static lazyMan getLazyMan(){
//对象唯一
if(LazyMan==null){
LazyMan = new lazyMan();
}
return LazyMan;
}
}
但由于创建对象并非原子性操作,多线程并发可能会创建多个对象,我们需要进一步优化
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;
}
但由于创建对象并非原子性操作,以上代码还存在问题,指令重排。
创建对象时,在JVM会经过三步:
(1)分配内存空间
(2)执行方法构造,初始化内存
(3)将对象指向分配好的这个空间
如果A线程创建对象的步骤为1、3、2,B线程在执行完1、3后进入,判定到LazyMan已不为空,return 获取到未初始化的LazyMan对象,则会造成空指针异常。
使用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;
}
}
但由于反射和序列化的存在,他们依然可以将单例对象破坏(产生多个对象),造成安全问题。
1:演示利用反射破坏单例模式
public static void main(String[] args) throws Exception {
// 获取类的显式构造器
Constructor 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
}
在 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()源码:
(2)防止反序列化: 在读入Singleton对象时,每个枚举类型和枚举名字都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,利用 Enum 类的 valueOf(String name) 方法根据变量的名字查找对应的枚举对象。
所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
小总结:
(1)Enum 类内部使用Enum 类型判定防止通过反射创建多个对象
(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象
(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。
————————————————
参考:https://blog.csdn.net/weixin_41949328/article/details/107296517