单例模式以及线程安全问题

单例模式的概念

单例模式是指的是整个系统生命周期内,保证一个类只能产生一个实例对象

保证类的唯一性 。

通过一些编码上的技巧,使编译器可以自动发现咱们的代码中是否有多个实例,并且在尝试创建多个实例的时候,直接编译出错。

应用场景:数据库连接池、多线程线程池 windows回收站

单例模式特点

单一实例 :单例模式确保一个类只有一个实例。
全局访问点 :单例模式提供了一个全局的访问点来获取这个唯一的实例。
延迟初始化 :单例模式通常实现延迟初始化,即在实际需要实例之前不会创建实例,这样可以节省系统资源。
线程安全:在多线程环境中,单例模式需要确保即时在高并发的情况下也能保持实例的唯一性。

单例模式实现方式

单例模式有很多种实现方式,包括饿汉式和懒汉式 。

懒汉式会涉及到线程安全问题 可以使用加锁的方式可以解决线程安全。

而饿汉式就不会有线程安全问题

下面我们用代码来分别实现一下饿汉 和懒汉的单例模式:

java 复制代码
package thread;
//单例模式  懒汉模式的实现

class SingletonLazy {
    private static  SingletonLazy instance = null;
    private static Object locker = new Object();

    public static SingletonLazy getInstance() {
      //  if (instance == null) {
            //synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
           // }
       // }
        return instance;
    }
    public SingletonLazy() {}
}
public class Demo28 {

    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}
java 复制代码
package thread;
//单例模式  懒汉模式的实现

class SingletonLazy {
    private static  SingletonLazy instance = null;
    private static Object locker = new Object();

    public static SingletonLazy getInstance() {
      //  if (instance == null) {
            //synchronized (locker) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
           // }
       // }
        return instance;
    }
    public SingletonLazy() {}
}
public class Demo28 {

    public static void main(String[] args) {
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

单例模式多线程环境下的安全问题

在多线程中 饿汉式单例模式是线程安全的 而 懒汉式单例模式是线程不安全的

为什么呢 因为饿汉式写法 创建实例的时机是在Java线程启动(比main调用还早的时机)再后续线程执行获取对象的时候 意味着实例早就已经存在了 每个线程的获取操作就做了一件事 读取代码中静态变量的值

多个线程读取同一个变量的值 线程是安全的

懒汉式 则涉及到读和修改操作 就是要先判断instance里面的引用地址是否为空 为空才修改 多线程环境下可能就会产生bug

像上面图片这种执行顺序就会出现线程安全问题 就不止一个实例了 就不符合我们单例模式的初衷了。

那怎么办呢 我们最容易想到处理的方式就是加锁了 那要怎么加锁呢

比如这样加锁:

这样很明显还是线程不安全:

因为这个锁就相当于没加 两个线程还是会new两个对象 那怎么办呢 我们可不可以把if判断操作和new操作打包成一个原子 答案是当然可以。

像上面这样加锁 t1执行加锁之后 ,t2就会阻塞等待 直到t1释放锁(new完了)t2才拿到锁,才能进行条件判读 t2判断的时候instance早就非空了 ,也就不会再new了。

但是现在还存在一个问题 就是我们上面那种加锁虽然解决了线程安全问题 但是这样设计锁 每次调用那个getinstance方法,就需要先加锁,再执行后续操作。 但是懒汉模式只是一开始调用的时候存在线程安全问题 ,一旦实例创建好了,后续再调用就只是读取操作了 ,就不存在线程安全问题

但是我们这样加锁就会出现后面都没有线程安全问题了 但是我们还在加锁,这就有点画蛇添足了。因为锁本身也是有开销的可能会使线程阻塞。

那怎么办呢 我们可以引入双重if判定

在上面这种加锁方式下 首先我们要先判断一下是否需要加锁 实例化之后线程安全了就不用加锁了 实例化之前就应该加锁 在两个if判断之间,synchronized会使线程阻塞等待 阻塞过程其他线程会修改instance的值

下面我们来画个时间轴来解释:

当t2在进去第一个if条件之后就会阻塞等待 等到t1释放锁 现在instace已经不为null了 t2的第二个if条件也是进不去的 后面不为空了 锁就不用加了。

这样就解决了没有线程安全也加锁的情况了。

但是现在还有一个问题 就是内存可见性引起的线程安全问题 就相当于

t1线程修改了instance引用,t2有可能读不到(不过这种概率应该很小)为了避免这种情况的发生 我们还要加上volatile 关键字

这个关键字还可以解决指令重排序问题

指令重排序

指令重排序也是编译器的一种优化策列 按照正常来说你写一段代码 cpu应该使按照顺序一条一条执行的 ,但是编译器就比较智能,会根据实际情况生成二进制指令的执行顺序,和你最初写的代码的顺序可能会存在差别

调整顺序最主要的目的就是为了提高效率,但是在保证逻辑是等价的。

上面执行这个代码会有三条指令

1、申请内存空间

2、调用构造方法(对内存空间进行初始化)

3、把此时内存空间的地址,赋值给instance引用

在多线程环境下 如果执行顺序是1 3 2 就会 出现线程安全问题

如果3指令比2指令先执行就会出现返回未初始化完毕的对象

就相当于t1线程执行完 instance就不是null了 但其实他是一个为初始化的对象 到时候t2线程执行的时候instance引用已经不是空的了 就进不去 就直接返回instance 了 返回了一个没有初始化完毕的对象 。这样就会导致很严重的线程安全问题 所以我们要加上volatile关键字 这样就很好的解决了指令重排序引起的线程安全问题。

总结

上面就是单例模式的相关实现和线程安全问题 当然单例模式还有很多延伸问题 怎么解决反射下能够保证是单例的模式 即使使用反射也不能破坏单例模式的唯一性呢 那可能就要用到枚举的实现 。但是我们上面讲的单例模式的内容 是经常用到的 在面试中也是会经常问到的 一般HR会让你现场写一个单例模式 你应该一步一步写 先不考虑线程安全问题 ,等着HR问你 你再慢慢一步一步加上解决的实现方法 。

谢谢大家的浏览 !!!

相关推荐
ishangy26 分钟前
集装箱箱底连锁检测:AI防爆摄像机保障智慧港口吊装安全
人工智能·安全·视觉检测·智慧港口·ai视频监控
YOU OU31 分钟前
SpringBoot 配置文件
java·spring boot·后端
c++之路32 分钟前
观察者模式(Observer Pattern)
java·网络·观察者模式
Dicky-_-zhang34 分钟前
云原生存储与数据库选型实战:从传统数据库到云原生数据库的演进
java·jvm
星幻元宇VR34 分钟前
VR消防安全体验屋|沉浸式科技助力消防安全科普
人工智能·科技·学习·安全·vr
凝小飞38 分钟前
cucumber JAVA 一键部署指南
java·集成测试·模块测试
java修仙传39 分钟前
Java 实习日记:断面状态筛选 Bug 修复与对比案例日期过滤优化
java·bug·实习
长谷深风11141 分钟前
Java并发编程:线程安全与多线程实战指南【个人八股】
java·安全·线程·进程·juc·并发与并行·上下文切换(性能影响因素)
Navicat中国1 小时前
使用 SSL/TLS 安全连接数据库
数据库·安全·ssl
basketball6161 小时前
C++ 强制类型转换:从 C 风格到 C++ 四大金刚
java·c语言·c++