Java Web 实战 04 - 多线程基础之线程安全问题

大家好 , 这篇文章带大家分析一下线程安全问题 . 线程安全问题在面试过程十分重要 , 而且这个问题也往往会被大家忽略 , 所以这一部分的知识希望大家认真学习
访问该页面阅读效果更佳 点击链接即可跳转
上一篇文章也给大家贴在这里了 点击即可跳转到上一篇文章
觉得文章不错的话 , 欢迎一键三连~
在这里插入图片描述

四 . 线程安全(重点)

多线程编程中 , 最重要也是最困难的问题就是多线程的线程安全问题

线程安全问题 , 最根本的问题就是调度器的 随机调度/抢占式执行 这个过程
那么线程不安全就是指 : 在随机调度之下 , 程序的执行就有了许多可能 , 然后其中的某些可能就产生了错误的结果

比如 : 购物平台推出了一个商品 , 限量 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 , 但是事实真的是这样吗 ?
image.png
那么这个 bug 是怎么样产生的呢 ?

4.2 线程安全问题是怎样产生的

这就需要我们再回顾一下计算机的基本组成结构
在这里插入图片描述

我们之前介绍过的数据库中的事务隔离性 , 其实就是并发导致的问题
(数据库内部并发执行事务 , 也是靠多线程的方式)
此处的线程安全问题 , 也是并发导致的问题 , 归根结底就是随机调度执行的结果
线程安全问题 , 是面试中相当高频的问题 , 也是工作中非常常见的问题


那么我们刚才说 , 数据应该在 5w~10w 之间
极端情况下 : 如果所有的指令排列恰好都是前两种 , 此时总和就是 10w
极端情况下 , 如果所有的指令排列都不是前两种 , 此时总和就是 5w
更实际的情况下 , 调度器具体调度出多少次前两种情况 , 多少次后面的情况 , 都是不确定的
所以数据一定会在 5w~10w 之间

4.3 造成线程不安全的原因

操作系统的随机调度 / 抢占式执行

这是万恶之源 / 罪魁祸首 , 我们无能为力

多个线程修改同一个变量

里面有几个关键字 :

  1. 多个线程 : 如果只是一个线程修改变量 , 没事
  2. 修改 : 如果是多个线程读同一个变量 , 也没事
  3. 同一个变量 : 如果是多个线程修改不同的变量 , 还没事

image.png
在写代码的时候 , 我们就可以针对这个要点进行控制了
可以通过调整程序的设计 , 破坏上面的条件
但是适用范围有限 , 不是所有的场景它都能规避掉

有些修改操作 , 不是原子的

原子 : 不可拆分的最小单位 , 比如 赋值(=) 操作 , 只对应一条指令 , 就视为是原子的
像我们刚才的 ++ 指令 , 它对应三条机器指令 , 则不是原子的

内存可见性引起的线程安全问题

这条原因 , 就是另外一个场景了 : 一个线程修改 , 一个线程读
这种场景就特别容易引起内存可见性 , 引发问题
举个栗子 :
image.png

这样的优化 , 是咋优化的 , 都优化出 Bug 了 , 还不如不优化呢 .
上述场景的优化 , 在单线程环境下 , 没问题 .
在多线程复杂的环境下 , 编译器 / JVM / 操作系统 … 在进行优化的时候就很有可能进行了误判
针对这个问题 , Java 引入了 volatile关键字 , 让程序员手动的禁止对某个变量进行优化

指令重排序

指令重排序也是 操作系统/编译器/JVM… 的优化操作 , 他是调整了代码的执行顺序
指令重排序就是他指令的顺序给调整了 , 达到加快速度的效果
举个栗子 :
image.png
按照上述的操作 , 调整顺序之后 , 也达到了我们的目标 , 也加快了效率
但是这个操作也有可能出现问题 :

Test t = new Test();

这简单的一句代码 , 底层对应着三条指令

  1. 创建内存空间
  2. 往这个内存空间上构造一个对象
  3. 把这个内存的引用赋值给 t

image.png

小结

线程安全问题的五种原因 :
前三种原因更普遍

  1. 系统的随机调度 [万恶之源 , 无能为力]
  2. 多个线程之间同时修改同一个变量 [可以部分规避]
  3. 修改操作不是原子的 [有办法改善] : 可以通过加锁操作 , 就可以把一些不是原子的操作打包成一个原子操作(数据库中的事务也是这样的) , Java 中加锁最常见的方式就是通过 synchronized关键字[稍后讲解]

后两种原因 , 就是优化搞出的幺蛾子 [编译器优化还是利大于弊的]

  1. 内存可见性
  2. 指令重排序

这两种情况是 编译器/JVM/操作系统… 误判了 , 把不应该优化的地方给优化了 , 执行逻辑就变了 , 所以 Bug 就出现了 , 我们可以使用 volatile规避掉

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

加勒比海涛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值