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

在多线程中 饿汉式单例模式是线程安全的 而 懒汉式单例模式是线程不安全的
为什么呢 因为饿汉式写法 创建实例的时机是在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问你 你再慢慢一步一步加上解决的实现方法 。

谢谢大家的浏览 !!!

上一篇:权限提升-Linux系统权限提升篇&Vulnhub&Rbash绕过&Docker&LXD容器&History泄漏&shell交互


下一篇:金融中的数学模型