【多线程】线程安全的单例模式

线程安全的单例模式

单例模式能保证某个类在程序中只存在 唯一 一份实例, 而不会创建出多个实例,从而节约了资源并实现数据共享。

比如 JDBC 中的 DataSource 实例就只需要一个.

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

饿汉模式

类加载的同时, 创建实例.

javascript 复制代码
    class Singleton {
        private static Singleton instance = new Singleton();
        // 私有化构造方法,防止外部创建实例
        private Singleton() {}
        public static Singleton getInstance() {
            return instance;
        }
    }

注意:

  1. 使用 static 修饰 instance,该实例就是该类的唯一实例。
  2. 要私有化构造方法,防止外部创建实例。
  3. 饿汉模式中,线程只读取了实例,所以是线程安全的。

懒汉模式

单线程版

类加载的时候不创建实例. 只有真正第一次使用它的时候才创建实例.

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

多线程版

上面的懒汉模式的实现是线程不安全的. (线程安全问题详解)

因为这里面 读取 和 修改 instance 是两个操作,不是原子操作,线程安全问题发生在首次创建实例时.

如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.

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

注意:

针对 Singleton 类对象加锁(类对象在一个程序中只能有一份),保证所有线程调用 getInstance 方法时,针对同一个对象进行加锁。

多线程版(改进)

代码可能出现线程安全问题的时机就在第一次创建实例时,一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改 instance 了) ,按照上面的加锁方式,不管是否会发生线程安全问题都会加锁,即使初始化之后线程安全了,仍然存在大量锁竞争,降低了程序的效率。

所以在加锁的基础上, 做出了进一步改动:

  • 使用双重 if 判定, 降低锁竞争的频率 。
  • 给 instance 加上了 volatile, 保证内存可见性以及防止指令重排序。
javascript 复制代码
class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么要双重 if 判定 ?

加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全 只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.

外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了,如果已经创建出来了就不用再加锁了。

为什么要使用 volatile ? volatile 关键字详解

  1. 保证内存可见性:(内存可见性问题)

多个线程调用 getInstance 方法,就会造成大量读 instance 内存操作,这样就可能导致编译器进行优化,不读内存,直接读寄存器,一旦优化,即使其他线程创建了实例,该线程也感知不到。所以使用 volatile 关键字。

(主要针对外外层的 if 判断,因为 synchronized 也能防止指令重排序,所以 内层判断不会受影响。)

  1. 防止指令重排序

什么是指令重排序?

举个栗子:

一段代码是这样的:

javascript 复制代码
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递

为了提高效率, JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台,提高效率。这种就叫做指令重排序。

编译器对于指令重排序的前提是 "保持逻辑不发生变化".

这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了,
多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测,
因此激进的重排序很容易导致优化后的逻辑和之前不等价.

其中创建实例 new Singleton() 又分为 三个步骤:

  1. 分配内存空间
  2. 对内存空间进行初始化
  3. 把内存空间的地址赋给引用 instance

假如没有使用 volatile 关键字,编译器可能对此进行了优化,进行了指令重排序,那么有可能优化为 1 -> 3 -> 2 。

这样的话,当第一个线程 t1 要获取实例时,因为实例为null, 所以肯定会创建实例,但是可能编译器进行了优化,那么可能顺序就变成了 1 -> 3 -> 2

  • 先开辟了一块空间
  • 将空间地址赋值给引用
  • 对空间初始化

当进行完第二步,把空间地址赋值给引用后,还没来得及初始化,此时另外一个线程 t2 来获取实例了, 进行判断时,发现 instance 不为空,那么就直接返回实例了

t2 拿到实例后,直接进行使用,那么就会报错了,因为虽然开辟了空间,但是 t1 还没来得及对空间进行初始化,拿到的是不完整的对象。

解决:

对 instance 对象加上 volatile 关键字,禁止指令重排序,保证其他线程拿到的是一个完整的实例。

完整过程举栗:

  1. 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁.

  2. 其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例还没有创建, 于是就把这个实例创建出来.

  3. 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了.

  4. 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从而不再尝试获取锁了. 降低了开销.

总结:

  1. 构造方法私有化,防止外部创建实例。
  2. 使用 static 修饰,保证是该类的唯一实例。
  3. 使用 volatile 修饰,保证内存可见性以及防止指令重排序。
  4. 双重 if 判断,第一次判断是否需要加锁,从而降低锁竞争,提高效率。
    第二层 if 判断是否真的需要创建实例。
  5. 使用 synchronized 进行加锁,防止第一次创建实例时由于线程安全问题而创建出多个实例。
相关推荐
安全系统学习5 小时前
【网络安全】Mysql注入中锁机制
安全·web安全·网络安全·渗透测试·xss
深圳安锐科技有限公司9 小时前
深圳安锐科技发布国内首款4G 索力仪!让斜拉桥索力自动化监测更精准高效
运维·安全·自动化·自动化监测·人工监测·桥梁监测·索力监测
潘锦9 小时前
海量「免费」的 OPENAI KEY,你敢用吗?
安全·openai
冰橙子id9 小时前
linux系统安全
linux·安全·系统安全
上海锝秉工控11 小时前
防爆拉线位移传感器:工业安全的“隐形守护者”
大数据·人工智能·安全
你不知道我是谁?12 小时前
AI 应用于进攻性安全
人工智能·安全
薄荷椰果抹茶13 小时前
【网络安全基础】第一章---引言
安全·网络安全
zskj_zhyl14 小时前
智绅科技:以科技为翼,构建养老安全守护网
人工智能·科技·安全
zsq14 小时前
【网络与系统安全】域类实施模型DTE
网络·安全·系统安全
vvilkim15 小时前
Electron 安全最佳实践:构建安全的桌面应用
javascript·安全·electron