【设计模式】详解单例设计模式(包含并发、JVM)

文章目录

1、背景

在软件开发中,经常需要某些类只能有唯一的实例 ,比如数据库连接。如何才能保证整个应用中只有一个唯一实例?如果靠人为制定的协定来约束,显然不能很好的保证这一点。如果要从语法上约束 ,在面向对象里面,什么地方能够约束实例的创建?

显然,只有构造函数类实例 的创建相关。那么如何才能让构造函数阻止类实例的创建,使其只有一个唯一实例?让构造函数的修饰为私有

2、单例模式

单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,"阻止"所有想要生成对象的访问。

3、代码实现

1、第一种实现(饿汉式)

java 复制代码
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
} 

饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可。类在加载时会在JVM的堆内存中创建一个Singleton对象,当类被卸载时,Singleton对象也随之消亡了(没有被引用)。

它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

为什么属性都是static的?

为了使其只有一个唯一实例,我们将无参构造方法设置成静态的,其他的类就没有办法直接通过new来构建该类的对象。无法通过new来构建类对象,那就只能通过调用类的静态方法getInstance这个唯一进出口来获得对象。由于静态方法里只能使用静态(static)属性,所以instance被修饰为静态的(static)。

2、第二种实现(懒汉式,线程不安全)

java 复制代码
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
} 

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

为什么不支持多线程?

假如现在有两个线程,一个线程a,一个线程b,线程a被操作系统选中,分配时间片,去调用Singleton.getInstance(),判断instance== null,为true,进入代码块 ,然后时间片用完,切换到线程b执行,线程b也调用Singleton.getInstance(),当运行到instance==null时,因为线程a上次判断完instance为null,就结束了,所以此时instance还是null,于是线程b执行instance=new Singleton(),并返回instance。线程b时间片用完,让出cpu,线程a被选中,从instance=new Singleton开始执行, 于是又在堆中创建了一个Singleton对象实例,并返回。所以严格意义上说它并不算单例模式。

3、第三种实现(懒汉式,线程安全)

java 复制代码
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance()
    { if (instance == null) {  
        instance = new Singleton();   }  
    return instance;   }  
} 

这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。因为在getInstance方法上加了synchronized锁,调用该方法的线程需要排队调用。(一个线程获得了Singleton类锁后,在该线程执行完getInstance()方法前,其他线程要么阻塞,要么自旋,无法执行getInstance方法)

4、第四种实现(懒汉式,双重校验锁DCL)

java 复制代码
public class Singleton {  
    private volatile static Singleton singleton;  //1
    private Singleton (){}  //2
    public static Singleton getSingleton() { //3 
    if (singleton == null) {  //4
        synchronized (Singleton.class) { //5 
            if (singleton == null) {  //6
                singleton = new Singleton(); //7 
            }  
        }  
    }  
    return singleton;  
    }  
}

第四种实现对于第三种实现进行了优化,如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例,这种优化提高了高并发下的性能。

getSingleton里为什么会有两个if判空?

假设现在有3个线程:线程A、线程B、线程C。线程A执行第一个if判断为空后,时间片用完让出cpu,切换线程。线程B执行第一个if判断为空后,加锁,加锁后恰好时间片用完,让出cpu。切换到线程C ,判断完第一个if后,因为B没有释放锁,等待时间片结束(cpu空转),切换线程。当再次切换到线程B后,进入同步代码块,先判断singleton是不是null,是null,创建对象,释放锁,返回对象。当执行完第一个if判断后阻塞在加锁的线程 ,抢到锁后,继续执行剩下的代码,在第二个if判断时,因为第一个线程已经创建了对象,则跳过,退出同步代码块,释放锁,得到线程B创建的实例对象。后面再来的线程在第一个if判断返回false后,直接返回线程B创建好的实例对象。

由此可见:第一个if是为了验证是否已经创建了对象,该判断是为了避免不必要的同步,第二个if是为了避免重复创建单例,是给第二个以及后面持有锁的线程准备的。

singleton为什么被volatile修饰

什么是指令重排序:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。

new对象操作在指令层面不是一个原子操作,分为三步:

  1. 为singleton分配内存空间M
  2. singleton初始化
  3. 将singleton指向分配好的内存空间M

在执行new操作时,2、3步可能发生指令重排,会发生下面这种情况:申请空间后,存入地址,但singleton对象还未初始化,切换线程。当新来的线程进行第一个if判断时,因为方法区中singleton里面是有堆中地址的,判断不为null,当return时,因为申请的空间中没有数据,则报空指针异常。

如下图:

使用volatile关键字可以防止指令重排序。使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换。使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

我在这里对堆、方法区、Java虚拟机栈进行了详解😄😄😄😄

相关推荐
后端小张1 分钟前
【JAVA 进阶】SpringAI人工智能框架深度解析:从理论到实战的企业级AI应用开发指南
java·开发语言·人工智能
麦烤楽鸡翅9 分钟前
小红书推荐系统(牛客)
java·python·算法·秋招·春招·牛客·面试算法题
C++业余爱好者23 分钟前
.NET线程池ThreadPool.QueueUserWorkItem
java·数据库·.net
.豆鲨包26 分钟前
【Android】Android内存缓存LruCache与DiskLruCache的使用及实现原理
android·java·缓存
superlls26 分钟前
(Java基础)集合框架继承体系
java·开发语言
宋哈哈31 分钟前
页面水印sdk源码
java·前端·javascript
你不是我我1 小时前
【Java 开发日记】我们来说一下 Mybatis 的缓存机制
java·spring·mybatis
咪咪渝粮1 小时前
112.路径总和
java·数据结构·算法
WKP94181 小时前
原型设计模式
java·设计模式
笃行客从不躺平1 小时前
SQL 注入复习
java·数据库·sql