「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
1.模式定义
在内存中只会创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了避免频繁的创建对象从而导致内存飙升。单例模式的作用就是在内存中仅创建一次对象,让所有调用它的地方共享这个对象。
单例模式有两种形式:
- 懒汉式:类加载后先不去创建实例对象,在被调用的时候再进行实例的创建。
- 饿汉式:类加载时立即创建实例对象,等待被调用。
2.模式结构
3.懒汉式代码分析
- 懒汉式的运行流程是,在调用前先判断对象是否被实例化,如果已经被实例化则直接返回该对象,如果没有被实例化则先创建实例再返回。
/**
* 懒汉式单例模式
*/
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
- 上面的代码创建了一个常规的懒汉式单例模式,可以运行但是存在问题。如果有两个线程同时调用这个实例那
Signleton就会被创建两个实例,这显然已经不再是单例模式了,那要解决这个问题最好的方式就是加锁,加锁后的代码如下所示:
/**
* 懒汉式单例模式
*/
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
}
- 上面的代码已经不会出现被创建多个实例的问题了,但是因为加了锁会导致每次调用都要先获取锁,这显然降低了效率,所以可以改成这样做:
/**
* 懒汉式单例模式
*/
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 上面的代码解决了多个线程访问时会创建多个实例且影响效率的问题,当程序调用实例对象时先判断对象是否为空,为空则进入下一行代码获取锁,多个线程时只会有一个线程获取到锁,获取后再进行判断并创建实例对象当第二个线程获取到锁的时候实例对象已经不为空并直接返回实例对象。
- 这样一个完整的懒汉式单例模式就实现了,但是还存在一个指令重排的问题。
- 指令重排是指:JVM在保证最终结果正确的情况下,可以不按照编码顺序执行语句,尽可能提高程序性能。详细介绍在这里
使用volatile可以防止指令重排
- 创建一个对象,在JVM中的流程有三步:
- 给
Singleton分配内存空间; - 初始化
Singleton; - 将Singleton指向分配好的内存空间
- 给
- 在创建一个对象的3个步骤中,2、3步是最有可能出现指令重排的现象,那么如果有多个线程获取对象时,线程A创建过程中执行步骤为1、3、2,线程B判断
Singleton已经不为空,获取到未初始化的Singleton对象,此时就会出现NPE的异常
- 使用
volatile的作用是什么:- 使用
volatile修饰的变量可以保证它的指令执行顺序与程序指明的顺序一致,不会发生顺序变化,进而避免NPE的发生; - 使用
volatile修饰的变量可以保证其内存可见性,每一次读取到该变量的值都可以保证是最新的,线程每次操作该变量都要先读取这个变量。
- 使用
- 代码实现如下:
/**
* 懒汉式单例模式
*/
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
4.饿汉式代码分析
饿汉式的运行是在类加载的时候就创建实例对象,等待被调用即可
/**
* 饿汉式单例模式
*/
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
//因为在类加载的时候就已经创建好了实例对象,因此它不会存在创建多个实例的情况,在类加载时会在堆内存中
//创建一个Signleton对象,当类被卸载时对象也会随之消失。
5.模式分析
单例模式中单例类要保证有一个私有的构造函数,确保用户无法通过new来创建一个实例,还要要有一个静态的私有成员变量与一个共有的工厂方法,这个工厂方法在饿汉式中返回实例,懒汉式中负责创建实例对象并返回。
6.优点
- 节约系统资源,对于需要频繁创建和销毁的的对象,单例模式可以提高其性能;
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
7.缺点
- 扩展困难,因为没有抽象类;
- 单例类的职责过重,除了提供一个工厂类之外还要负责一些业务方法,将产品的创建和本身的功能融合到了一起。
8.适用环境
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。
参考链接: