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

1、背景

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

2、单例模式

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

3、代码实现

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

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、第二种实现(懒汉式,线程不安全)

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、第三种实现(懒汉式,线程安全)

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)

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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值