【 多线程案例 - 单例模式 】

一、了解单例模式

单例模式是校招中最常考的设计模式之,单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例

这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个

那什么是设计模式?

设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有
一些固定的套路. 按照套路来走局势就不会吃亏.软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏

单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

我们还可以回忆下 枚举实现线程安全的单例模式!!

二、饿汉模式

类加载的同时, 创建实例 ,用到这个类就会立即加载,比较着急地去创建实例

//饿汉模式:都是线程安全的
class Singleton {
    //1、使用 static 创建一个实例,并且立即进行实例化.这个instance对应的实例就是该类的唯一实例
    //类加载阶段就会直接创建实例(程序中用到该类就会立即加载)
    private static Singleton instance = new Singleton();
    //2、把构造方法设置为私有的,防止其他地方对 Singleton 进行实例化
    private Singleton() {

    }
    //3、提供一个方法,让外面能够拿到唯一实例
    public static Singleton getinstance() {
        return instance;
    }
}



注意:

  1. 饿汉模式都是线程安全的,其中getinstance仅仅是读取了变量的内容,当多个线程只是读取一个变量的时候也是线程安全的
  2. 使用 static 创建一个实例,并立即进行实例化.这个instance对应的实例就是该类的唯一实例

为什么使用static创建实例就能保证是唯一实例呢?
因为static修饰的成员是"类成员",一个java程序中,jvm会保证只有一个类对象,进一步在饿汉模式中也就保证了类的static成员也是只有一份

  1. 把构造方法设置为私有的,防止其他地方对 Singleton 进行实例化。私有的构造方法无法在本类外部使用,也就导致本类无法用new实例化,这样可以控制对象的生成。

三、懒汉模式

3.1 多线程版本(线程不安全)

class Singleton1 {
    //1、不是立即就初始化实例
    private static Singleton1 instance = null;

    //2、将构造方法设置为私有的
    private Singleton1() {

    }
    //3、提供一个方法来获取实例.只有当我们真正用到这个实例的时候才会去创建这个 实例
    public static Singleton1 getInstance() {
        if (instance == null) {
            instance = new Singleton1();
        }
        return instance;
    }
}

注意:

在懒汉模式中,getinstance既包含了读,又包含了修改。而且读和修改还不是原子的,所以并不是线程安全的,因此也会创建出多个实例

上述线程不安全具体指的是在多线程的环境下,并发的调用getinstance如下:

在这里插入图片描述

初始的instance是null,加载到CPU中与if条件中的null进行比较,满足相等就会创建出一个实例。由于线程的抢占式执行,导致进行了两次比较且相等,于是就创建出了两个实例。

那如何解决该问题呢?加上 synchronized 可以改善这里的线程安全问题.

在这里插入图片描述

这里是针对Singleton类对象进行加锁,这个类对象在程序中只有一份,在多个线程调 getinstance的时候就可以保证是针对同一个对象加锁!

3.2 多线程版本改进(线程安全)

对于上面的问题,我们对其进行加锁后达到了线程安全。但是又出现了新的问题,如下:

对于前面懒汉模式的代码来说,线程不安全发生在instance被初始化之前,由于多线程调用 getinstance ,就可能同时涉及到读和修改操作。但是一旦instance被初始化之后,就只剩下了读操作,此时也就线程安全了。

所以按照上述加锁方式,无论代码是初始化之前还是初始化之后,每次调用getinstance时都会进行加锁,也就意味着即使初始化之后是线程安全的,但还是存在着大量不必要的锁竞争!!

改进方案:

调用getinstance,instance 初始化之前才进行加锁,而初始化之后就不再加锁了。所以只需要在外层再加一次条件判断即可,判断当前instance是否已经初始化完成,如下:

    public static Singleton2 getInstance() {
        //判断是否初始化,若没有初始化就存在线程不安全,得加锁
        if (instance == null) {
            synchronized (Singleton2.class) {
                if (instance == null) {//判断是否要创建实例
                    instance = new Singleton2();
                }
            }
        }
        return instance;
    }

解决大量锁竞争的问题后,此时还存在一个很大的缺陷,如下:

执行 instance = new instance() 时,需要经历三个步骤,1.创建内存 2.针对内存进行初始化 3.把内存地址赋值给引用.而在这里是有可能触发指令重排序的,乱序执行. 若执行顺序变为了1-3-2,此时其他线程就可能看到一个非null的引用,但实际访问的时候却会出现一个非法的现象.

解决办法: 给 instance 加上 volatile关键字,如下:

private static volatile Singleton2 instance = null;

完整代码:

在这里插入图片描述

注意: 两个if中的条件并不一样,这里只是巧合

以上就是线程安全的懒汉模式,最核心的三个地方,1.在合适的地方加锁 2. 进行双重if判定
3.加上volatile关键字


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值