文章目录
大家好 , 这篇文章带大家分析一下线程安全问题 . 线程安全问题在面试过程十分重要 , 而且这个问题也往往会被大家忽略 , 所以这一部分的知识希望大家认真学习
访问该页面阅读效果更佳 点击链接即可跳转
上一篇文章也给大家贴在这里了 点击即可跳转到上一篇文章
觉得文章不错的话 , 欢迎一键三连~
四 . 线程安全(重点)
多线程编程中 , 最重要也是最困难的问题就是多线程的线程安全问题
线程安全问题 , 最根本的问题就是调度器的 随机调度/抢占式执行 这个过程
那么线程不安全就是指 : 在随机调度之下 , 程序的执行就有了许多可能 , 然后其中的某些可能就产生了错误的结果
比如 : 购物平台推出了一个商品 , 限量 200 份 , 如果某一个用户抢购成功 , 库存就 - 1
但是限量 200 份 , 肯定会有很多人抢 , 所以会使用到多线程
如果线程不安全 , 就会有很多人抢到了鞋子 , 而库存才 -1 , 所以线程安全也是非常重要的
4.1 代码示例 : 线程安全举例
当两个线程对同一个变量进行并发式的自增 , 每次运行结果就不一样
// 创建两个线程,让这两个线程同时并发的对一个变量自增 5w 次
// 最终预期能够一共自增 10w 次
class Counter {
// 用来保存计数的变量
public int count;
public void increase() {
count++;
}
}
public class Demo14 {
// 这个实例的作用是为了进行累加
public static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count:" + counter.count);
}
}
我们期望代码的运算结果是 10w , 但是事实真的是这样吗 ?
那么这个 bug 是怎么样产生的呢 ?
4.2 线程安全问题是怎样产生的
这就需要我们再回顾一下计算机的基本组成结构
我们之前介绍过的数据库中的事务隔离性 , 其实就是并发导致的问题
(数据库内部并发执行事务 , 也是靠多线程的方式)
此处的线程安全问题 , 也是并发导致的问题 , 归根结底就是随机调度执行的结果
线程安全问题 , 是面试中相当高频的问题 , 也是工作中非常常见的问题
那么我们刚才说 , 数据应该在 5w~10w 之间
极端情况下 : 如果所有的指令排列恰好都是前两种 , 此时总和就是 10w
极端情况下 , 如果所有的指令排列都不是前两种 , 此时总和就是 5w
更实际的情况下 , 调度器具体调度出多少次前两种情况 , 多少次后面的情况 , 都是不确定的
所以数据一定会在 5w~10w 之间
4.3 造成线程不安全的原因
操作系统的随机调度 / 抢占式执行
这是万恶之源 / 罪魁祸首 , 我们无能为力
多个线程修改同一个变量
里面有几个关键字 :
- 多个线程 : 如果只是一个线程修改变量 , 没事
- 修改 : 如果是多个线程读同一个变量 , 也没事
- 同一个变量 : 如果是多个线程修改不同的变量 , 还没事
在写代码的时候 , 我们就可以针对这个要点进行控制了
可以通过调整程序的设计 , 破坏上面的条件
但是适用范围有限 , 不是所有的场景它都能规避掉
有些修改操作 , 不是原子的
原子 : 不可拆分的最小单位 , 比如 赋值(=) 操作 , 只对应一条指令 , 就视为是原子的
像我们刚才的 ++ 指令 , 它对应三条机器指令 , 则不是原子的
内存可见性引起的线程安全问题
这条原因 , 就是另外一个场景了 : 一个线程修改 , 一个线程读
这种场景就特别容易引起内存可见性 , 引发问题
举个栗子 :
这样的优化 , 是咋优化的 , 都优化出 Bug 了 , 还不如不优化呢 .
上述场景的优化 , 在单线程环境下 , 没问题 .
在多线程复杂的环境下 , 编译器 / JVM / 操作系统 … 在进行优化的时候就很有可能进行了误判
针对这个问题 , Java 引入了 volatile
关键字 , 让程序员手动的禁止对某个变量进行优化
指令重排序
指令重排序也是 操作系统/编译器/JVM… 的优化操作 , 他是调整了代码的执行顺序
指令重排序就是他指令的顺序给调整了 , 达到加快速度的效果
举个栗子 :
按照上述的操作 , 调整顺序之后 , 也达到了我们的目标 , 也加快了效率
但是这个操作也有可能出现问题 :
Test t = new Test();
这简单的一句代码 , 底层对应着三条指令
- 创建内存空间
- 往这个内存空间上构造一个对象
- 把这个内存的引用赋值给 t
小结
线程安全问题的五种原因 :
前三种原因更普遍
- 系统的随机调度 [万恶之源 , 无能为力]
- 多个线程之间同时修改同一个变量 [可以部分规避]
- 修改操作不是原子的 [有办法改善] : 可以通过加锁操作 , 就可以把一些不是原子的操作打包成一个原子操作(数据库中的事务也是这样的) , Java 中加锁最常见的方式就是通过
synchronized
关键字[稍后讲解]
后两种原因 , 就是优化搞出的幺蛾子 [编译器优化还是利大于弊的]
- 内存可见性
- 指令重排序
这两种情况是 编译器/JVM/操作系统… 误判了 , 把不应该优化的地方给优化了 , 执行逻辑就变了 , 所以 Bug 就出现了 , 我们可以使用 volatile
规避掉