• 多线程带来的的风险-线程安全


    v2-7bdded212ba4d459fb4fe10bcaa0021d_b

    多线程带来的的风险-线程安全

    ~~ 多线程编程中,最难的地方,也是一个最重要的地方,还是一个最容易出错的地方,更是一个面试中特别爱考的地方.❤️❤️❤️

    线程安全的概念

    万恶之源,罪魁祸首是多线程的抢占式执行,带来的随机性.~~😕😕😕
    如果没有多线程,此时程序代码执行顺序就是固定的,代码顺序固定,程序的结果就是固定的.
    如果有了多线程,此时在抢占式执行下,代码执行的顺序,会出现更多的变数!!!
    代码执行顺序的可能性就从一种情况变成无数种情况!!!
    所以就需要保证这无数种线程调度顺序的情况下,执行的结果都是正确的!!!只要是有一种情况下,代码结果不正确,就视为线程不安全!!!

    问题来了:能否消除这样的随机性了🤔🤔🤔?
    调度的源头来自于操作系统的内核实现.
    1.作为程序猿的我们改不了.😂😂😂
    2.即使改了自己的操作系统,也无法推广开来,因为全世界大多数操作系统都是这样的,已成定局!😕😕😕

    观察线程不安全(代码)😍😍😍

    class Counter{
        public int count = 0;
        public void add(){
            count++;
        }
    }
    public class ThreadDemo13 {
        public static void main(String[] args) {
            Counter counter = new Counter();
    
            // 创建两个线程, 两个线程 counter 来调用 5W 次的add方法
            Thread t1 = new Thread(()->{
                for (int i = 0; i < 5_0000; i++) {
                    counter.add();
                }
            });
            Thread t2 = new Thread(()->{
                for (int i = 0; i < 5_0000; i++) {
                    counter.add();
                }
            });
            // 启动线程
            t1.start();
            t2.start();
    
            // 等待两个线程结束
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
    
            // 打印最终的 count 值 预期结果: count = 10W
            System.out.println("count = "+ counter.count);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    运行结果:

    image-20230923031309538
    我们的需求是两个线程各自自增 5w次,一共自增 10w次,
    预期结果是 10w,实际结果不是 10w而且每次都不一样.
    程序出现了bug(程序不符合需求,就是bug).
    注:这个就是典型的线程安全问题!!!😥😥😥

    线程不安全的原因

    ~~ 为什么程序就出现了这个情况🤔🤔🤔?

    线程与指令之间的关系:

    一个线程要执行,就需要先编译成很多的CPU指令,写的任何一个代码都是要编译成很多的CPU指令的!!!
    个人理解:一个线程是来完成一个任务,要做一些工作,而这个工作是可以分解成一个一个的小步骤的,每个小步骤就是一个指令.
    由于线程的抢占式执行,导致当前执行到任意一个指令的时候,线程都有可能被调度走,然后CPU让别的线程来执行.

    寄存器,CPU里重要的组成部分,寄存器也能存数据,空间更小,访问速度更快,CPU进行的运算都是针对寄存器(准确的说,是通用寄存器,如EAX,EBX,ECX)中的数据进行的

    count++;
    ++ 操作本质上要分成三步
    1.先把内存中的值,读取到CPU的寄存器中 ~~load
    2.把CPU寄存器里的数值进行 +1运算 ~~add
    3.把得到的结果写回到内存中 ~~ save
    注:load,add,save就是CPU上执行的三个指令(被视为机器语言).

    两个线程并发的执行count++,此时就相当于两组load,add,save进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异.

    作图来理解多线程的调度:❤️❤️❤️

    image-20230923125739880

    分析执行过程:

    image-20230923133152779

    思考一下🤔🤔🤔:

    出现bug 之后,得到的结果一定是 <= 10w, 结果是一定 >= 5w 嘛?
    极端情况下,所有的执行都是交错执行,是否就是 5w 呢??
    实际上,结果是可以小于 5w ,只是概率更低了!!
    image-20230923133717932

    根结底线程安全问题全是因为==线程的无序调度(罪魁祸首,万恶之源)==导致了执行顺序不确定结果就变化了!!!

    总结(线程不安全的原因)😊😊😊

    1. [根本原因] 抢占式执行,随机调度 ~~ 对此,我们无能为力.

    2. 代码结构:

      • 多个线程 同时 修改 同一个变量 ~~ 不安全!!! 😥😥😥

      • 一个线程,修改一个变量,安全.

      • 多个线程读取同一个变量,安全.

      • 多个线程修改多个不同的变量,安全.

      • 注1: 因此可以通过代码结构来规避这个线程不安全问题,
        但是因为需求问题,代码结构无法进行调整(这种方法使用频率并不高).

      • 注2: 修改 => 不可变对象是无法修改的,天然就是线程安全的!!!

    3. 原子性. 如果修改操作是原子的,出现问题概率小;如果是非原子的,出现问题概率极高(线程不安全问题,其实本质上是事务的脏读问题,之前博主写的博客解释过相关概念,在此不做解释啦!ヾ(❀^ω^)ノ゙

      • 原子: 不可拆分的基本单位.

        上述 count ++ 操作就不是原子,里面可以拆分成三个操作,load,add,save.某个操作,对应单个CPU指令,就是原子的,如果这个操作对应多个CPU指令,大概率就不是原子的.比如直接使用 = 赋值,就是一个原子的操作

    4. 内存可见性,引起的线程不安全

      • 一个线程读,一个线程改,可能出现读的结果和预期不符合的问题.
    5. 指令重排序,引起的线程不安全

      • 本质上是编译器优化,优化出bug了.优化:编译器觉得程序猿写的代码太low了,对代码进行调整,在保持代码逻辑不变的情况下,调整代码的执行顺序,从而加快程序的执行效率.

    线程不安全问题的解决

    **如何解决线程不安全问题?**🤨🤨🤨🤨
    最主要的手段就是从这个原子性下手,通过加锁不能把非原子的操作变成原子的.
    即解决前面代码的线程不安全问题,就是通过加锁让count++变成原子的.
    synchronized public void add(){ count++; }

    加了 synchronized 之后,进入方法就会加锁,出了方法就会解锁.
    如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(处于BLOCKED状态),一直阻塞到刚才的状态解锁(释放锁),当前线程才能加锁成功!!! => 操作系统的基本设定,系统里的锁“不可剥夺”特性,一旦一个线程获取到锁,除非它主动释放,否则无法强占.

    加锁,说是保证原子性,不是让load,add,save三个操作一次完成,也不是就是让其它也想操作的线程阻塞等待了
    image-20230923205141702

    image-20230923203926850

    虽然加锁之后,算得慢了,但是还是比单线程要快,加锁只是针对count++加锁了,除了count++之外,还有for循环的代码,for循环代码是可以并发执行的(线程t1和线程t2各自修改各自for循环的局部变量i,是没问题的),只是count++串行执行了.
    一个任务中,一部分可以并发,一部分串行,仍然是比所有代码串行要快的.


    加锁,是要明确执行对哪个对象加锁的.如果两个线程针对同一个对象加锁,会产生阻塞等待(锁竞争/锁冲突),如果两个线程针对不同对象加锁,不会阻塞等待(不会锁冲突/锁竞争).

    synchronized的使用方法😊😊😊😊

    1. 修饰方法
      • 修饰普通方法 ~~ 把锁加到this对象上
        • 进入方法就加锁,离开方法就解锁
      • 修饰静态方法 ~~ 把锁加到类对象上
        • 通理也是这样,进入方法就加锁,离开方法就解锁
      • 但是这两种修饰方法,加锁的“对象”不同,修饰普通方法,锁对象就是this,修饰静态方法,锁对象就是类对象.
        image-20230924235103704
    2. 修饰代码块
      • 显示/手动指定锁对象
    3. 锁对象
      1. 明确锁对象,针对那个对象加锁
      2. 两个线程,针对同一个对象加锁,会发生锁冲突/锁竞争(产生阻塞等待)
        • 一个线程获取到锁(先到先得),另一个线程就得塞等待,等待到上一个线程解锁,它才能获取锁成功.
      3. 两个线程,针对不同的对象加锁,不会有任何锁冲突
        • 这两个线程都能获取到各自的锁,此时不会有阻塞等待了.
    • 两个线程,一个线程加锁,一个线程不加锁,这个时候就不存在锁竞争了.
      image-20230925000413172注: 这种写法和没加锁的运行结果一样.
  • 相关阅读:
    电脑技巧:Win10自带的6个实用功能,你都会用吗
    基于微信点餐外卖小程序系统设计与实现 开题报告
    【云原生】使用Docker快速部署openGauss主备环境
    CCCorelib 点云法向量计算之拟合曲面(CloudCompare内置算法库)
    7.zigbee开发,低功耗,通信加密开发
    代理IP、Socks5代理与网络工程:解析技术世界的无限可能
    用nativescript开发ios程序常用命令?
    【windows版】TensorRT安装教程
    【C++入门系列】——类和对象
    Python刷题系列(9)_Pandas_Series
  • 原文地址:https://blog.csdn.net/m0_73740682/article/details/133221556