设计模式实战(二)(单例模式)
设计模式实战
👾设计模式demo实战。
项目地址:https://github.com/bearbrick0/designpattern
🫑创建型设计模式
创建型的设计模式包括:单例模式、工厂模式、建造者模式、原型模式。它主要解决对象的创建问题。封装复杂的问题,解耦对象创建代码和使用代码。
🥦单例设计模式
在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。
单例模式用来创建全局唯一的对象。一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫做单例模式。单例有几种经典的实现方式,他们分别是:恶汉式、懒汉式、双重检测、静态内部类、枚举。
在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
单例模式的优缺点
单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
尽管单例模式是一个很常用的设计模式,在实际开发中,我们也确实经常用到它,但是,有些人认为单例是一种反模式,并不推荐使用,主要的理由有以下几点:
- 单例对OOP特性的支持不友好
- 单例会对隐藏类之间的依赖关系
- 单例对代码的扩展性不友好
- 单例对代码的可测性不友好
- 单例不支持有参数的构造函数
哪有什么替代单例的解决方案呢?
如果要完全解决这些问题,我们可能要从根本上寻找其他方式来实现全局唯一类。比如,通过工厂模式、IOC容器来保证全局唯一性。
有人把单例模式当作反模式,主张杜绝在项目中使用。我个人觉得这有点极端。模式本身没有对错,关键看你怎么用。如果单例模式并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大的问题,对于一些全局类,我们在其他地方new的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便。
除此之外,我们还讲到了进程唯一单例类、线程唯一单例类、集群唯一单例类、多例等扩展知识点,这一部分在实际的开发中并不会被用到,但是可以扩展你的思路、锻炼你的逻辑思维。这里我就不带你回顾了,你可以自己回忆一下。
单例模式的应用场景
对于 Java 来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,对 GC友好。
- 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 频繁访问数据库或文件的对象。
- 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
单例模式的结构与实现
单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。
但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。
这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
下面来分析其基本结构和实现方法。
单例模式的结构
单例模式的主要角色如下。
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
实现:
🥝懒汉模式实现单例模式
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance()
方法时才去创建这个单例
懒汉式(多线程下线程不安全)
(具体参考com/uin/creationPattern/Singleton/LazyBones/LazyMan
)
这种方式在单线程的环境下使用,对于多线程是无法保证单例的,这里列出来是为了和后面使用锁保证线程安全做对比。
package com.uin.creationPattern.Singleton.LazyBones.LazyMan;
/**
* 第一种方法 懒汉(线程不安全)
*/
public class Person {
private String name;
private int age;
/**
* 懒汉--需要的时候在创建对象
*/
private static Person instance;
/**
* 构造器私有,外部不能实例化
*/
private Person() {
}
/**
* 提供一个静态的公有方法,当使用该方法时才去创建instance
*/
public static Person wanglufeiPerson() {
//没有在去创建
if (instance == null) {
instance = new Person();
}
return instance;
}
}
package com.uin.creationPattern.Singleton.LazyBones.LazyMan;
/**
* \* Created with IntelliJ IDEA.
* \* @author wanglufei
* \* Date: 2021年08月27日 14:59
* \* Description:
* \
*/
public class MainTest {
public static void main(String[] args) {
//测试
Person person1 = Person.wanglufeiPerson();
Person person2 = Person.wanglufeiPerson();
System.out.println(person1 == person2);
//测试
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
/**
* 总结: 懒汉式(线程不安全)优缺点
* (1) 起到了Lazy Loadding的效果,但是只能在单线程的情况下使用
* (2) 如果在多线程下,一个线程进入了if(instance==null)判断语句块,还未来得及往下执行,另一个线程也通过了
* 这个判断语句这时就会产生多个实例。所以在多线程的情况下不可使用这种方式。
* 结论: 在实际开发中,不要使用这种方式。
*/
}
}
- 优点: 懒加载
- 缺点: 线程不安全
懒汉(加入同步锁线程安全)
(参考代码com/uin/creationPattern/Singleton/LazyBones/AddSynchronized/Singleton.
java)
懒汉式单例如何保证线程安全呢?通过 synchronized
关键字加锁保证线程安全, synchronized
可以添加在⽅法上⾯,也可以添加在代码块上⾯,这⾥演示添加在⽅法上⾯,存在的问题是每⼀次调⽤ getInstance()
获取实例时都需要加锁和释放锁,这样是⾮常影响性能的。
package com.uin.creationPattern.Singleton.LazyBones.AddSynchronized;
/**
* \* Created with IntelliJ IDEA.
* \* @author wanglufei
* \* Date: 2021年08月27日 16:58
* \* Description: 第二种方法 懒汉(解决线程不安全--加入了同步锁)
* \
*/
public class Singleton {
//1. 构造器私有化,外部不能创建实例化对象(new)
private Singleton() {
}
//2. 懒汉--需要的时候在创建对象
private static Singleton instance;
//3. 提供一个静态的公有方法,加入了同步处理的代码(同步锁),解决了线程不安全的问题
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public static void main(String[] args) {
//测试
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1==instance2);//true
System.out.println(instance1.hashCode());//460141958
System.out.println(instance2.hashCode());//460141958
/**
* 总结: 懒汉式(线程安全 加synchronized)优缺点
* (1) 解决了线程安全的问题
* (2) 效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例
* 化代码就够了,后面的项获得该类的实例,直接return就行了。但是你在getInstance()后面加了synchronized,在每一次都
* 会同步。方法进行的同步效率太低。
* (3) 结论:在实际开发中,不推荐使用这种方式
*
*/
}
}
- 优点:懒加载,线程安全
- 缺点:效率低
懒汉式(同步代码块)
(参考代码com/uin/creationPattern/Singleton/LazyBones/SynchonizedCodeBlock/Singleton.java
)
package com.uin.creationPattern.Singleton.LazyBones.SynchonizedCodeBlock;
/**
* \* Created with IntelliJ IDEA.
* \* @author wanglufei
* \* Date: 2021年08月27日 17:22
* \* Description: 第三种方法 懒汉式(同步代码块)
* \
*/
public class Singleton {
//1. 构造器私有化,外部不能创建实例化对象(new)
private Singleton() {
}
//2. 懒汉--需要的时候在创建对象
private static Singleton instance;
//3. 提供一个静态的公有方法,加入了同步代码块,解决不了线程安全的问题 但是还是效率太低
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
public static void main(String[] args) {
//测试
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);//true
System.out.println(instance1.hashCode());//460141958
System.out.println(instance2.hashCode());//460141958
/**
* 总结: 懒汉式(线程安全 加synchronized同步代码块)优缺点
* (1) 这种方式,本意是想对在静态方法加synchronized的该进,因为前面的同步方法的效率太低了,改为同步产生实例化
* 的代码块
* (2) 但是这种同步并不能起到线程安全的作用。和懒汉式LazyMan与遇到的情形一致。假如一个线程进入了
if(instance==null)判断语句模块,还未来的及往下执行,另个一个线程也通过了这个判断语句,这时就会产生多个实例。
* (3) 结论:在实际开发中,不能使用这种方式
*/
}
}
懒汉式双重检查(DCL)
双重检查锁(DCL, 即 double-checked locking)
参考代码(com/uin/creationPattern/Singleton/LazyBones/DoubbleCheck/Singleton.java)
Double-Check概念是多线程开发中常使用到的,如代码所示,我们进行了两次if(singleton==null)
检查,这样就保证了线程安全。
这样,实例化代码只用执行一次,后面再次访问时,直接if(singleton==null)
,为FALSE
,直接return
实例化对象,也避免了反复进行方法同步。
这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized
加锁,为什么要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回了单例。
关于内部的第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null,会进⾏单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。
其中最关键的⼀个点就是 volatile 关键字的使⽤,关于 volatile 的详细介绍可以在我的博客中搜索即可,这⾥不做详细介绍,简单说明⼀下,双重检查锁中使⽤ volatile
的两个重要特性:可⻅性、禁⽌指令重排序
这⾥为什么要使⽤ volatile
?
这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯的步骤:
-
在堆内存开辟内存空间
-
调⽤构造⽅法,初始化对象
-
引⽤变量指向堆内存空间
对应字节码指令如下:
为了提⾼性能,编译器和处理器常常会对既定的代码执⾏顺序进⾏指令重排序,从源码到最终执⾏指令会经历如下流程:
源码编译器优化重排序指令级并⾏重排序内存系统重排序最终执⾏指令序列所以经过指令重排序之后,创建对象的执⾏顺序可能为 1 2 3 或者 1 3 2 ,因此当某个线程在乱序运⾏ 1 3 2 指令的时候,引⽤变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进⼊了 getInstance 的第⼀个 if(instance == null) 判断不为 nulll ,导致错误使⽤了没有初始化的⾮ null 实例,这样的话就会出现异常,这个就是著名的DCL 失效问题。
当我们在引⽤变量上⾯添加 volatile 关键字以后,会通过在创建对象指令的前后添加内存屏障来禁⽌指令重排序,就可以避免这个问题,⽽且对volatile 修饰的变量的修改对其他任何线程都是可⻅的。
package com.uin.creationPattern.Singleton.LazyBones.DoubbleCheck;
/**
* \* Created with IntelliJ IDEA.
* \* @author wanglufei
* \* Date: 2021年08月27日 17:39
* \* Description: 第四种 懒汉双重检查
* \
*/
public class Singleton {
/**
* 当一个变量定义为volatile之后,它将具备两种特性
* 1. 保证此变量对所有线程的可见性
* 这里不做过多解释,简单的说就是,当一个线程修改了volatile变量之后,它先写入它的工作内存中,
* 然后立刻写入主内存,并且刷新其他线程中的工作内存,这样其他线程再去读取他们工作内存中的变量时,
* 确保能够拿到最新的。但是如果是普通变量的话,它不会立即写入主内存中,所有其他线程的工作内存中保存的是旧的值。
* 所有volatile变量可以保证可见性。
* 2. 禁止指令重排序优化
*/
private static volatile Singleton instance;
//1. 构造器私有化,外部不能通过new来实例化对象
private Singleton() {
}
//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
//测试
System.out.println(instance1==instance2);
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
- 优点:线程安全:延迟加载;效率较高
- 结论: 在实际开发中,推荐使用这种的单例设计模式
🥥饿汉模式实现单例模式
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance()
方法之前单例已经存在了。
饿汉式(静态变量)
package com.uin.creationPattern.Singleton.BadMash.StaticConstants;
/**
* \* Created with IntelliJ IDEA.
* \* @author wanglufei
* \* Date: 2021年08月27日 15:07
* \* Description: 第一种 饿汉式(静态变量)
* \
*/
public class singleton {
//1. 构造器私有化,外部不能创建实例化对象(new)
private singleton() {
}
//2. 在类内部创建实例化对象
private static final singleton instance = new singleton();
//3. 对外提供一个公有的静态方法,返回实例对象
public static singleton getInstance() {
return instance;
}
}
饿汉式静态代码块
package com.uin.creationPattern.Singleton.BadMash.StaticCodeBlock;
/**
* Created with IntelliJ IDEA.
* @author wanglufei
* Date: 2021年08月27日 15:41
* Description: 第二种 饿汉静态代码块
*/
public class singleton {
//1. 构造器私有化,外部不能创建实例化对象(new)
private singleton() {
}
//2. 在类内部创建实例化对象
//private final static singleton instance = new singleton();
private static singleton instance;
//在静态代码块中创建单例对象
static {
instance = new singleton();
}
//3. 对外提供一个公有的静态方法,返回实例对象
public static singleton getInstance() {
return instance;
}
public static void main(String[] args) {
//测试
singleton instance1 = singleton.getInstance();
singleton instance2 = singleton.getInstance();
System.out.println(instance1 == instance2); //true
/**
* 测试结果为:true
* 结论: 为同一个对象
*/
//===============================================
//测试
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
/**
* 测试结果为:460141958
* 结论: 说明这两个对象实例是同一个对象实例
*/
//************************************************
/**
* 总结:优缺点
* (1) 这种方式和上面的方式其实类似,只不过将实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静
* 态代码块中的代码,初始化类的实例。有缺点和上面是一样的。
*/
}
}
本文来自博客园,作者:{BearBrick0},转载请注明原文链接:{https://www.cnblogs.com/bearbrick0}