• 线程安全问题的原因和解决方案



    🚘线程安全问题的原因

    线程安全的意思是:在多线程各种随机的调度顺序下,代码没有bug,都能按照符合预期的方式来执行~~
    如果在多线程随机调度下,代码出现bug,此时就是认为线程不安全!
    导致线程不安全的几个因素:

    1. 操作系统调度的随机性,抢占式执行[万恶之源] , 但是无能为力
    2. 多个线程同时修改同一个变量 . 可以通过调整代码结构一定程度的规避这个原因
    3. 修改操作不是原子的(可以改良,核心思路,就是把这一组 ++ 操作变成原子的!!! 使用加锁操作~)
    4. 内存可见性
    5. 指令重排序

    🚞修改操作不是原子的

    典型线程不安全代码:

    每个线程循环 5w 次,累加变量 count 的值(⭕预期结果10w):

    /**
     * @Author YuanYuan
     * @Date 2022/9/11
     * @Time 10:58
     */
    
    class Counter {
        public int count = 0;
        public void increase() {
            count++;
        }
    }
    public class TestDemo {
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
            Thread thread = new Thread(()-> {
                for (int i = 0; i < 5_0000; i++) {
                    counter.increase();
                }
            });
            Thread thread1 = new Thread(()-> {
                for (int i = 0; i < 5_0000; i++) {
                    counter.increase();
                }
            });
            thread.start();
            thread1.start();
    
            thread.join();
            thread1.join();
    
            System.out.println("counter :" + 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

    在这里插入图片描述

    ❓上述问题是怎么出现的呢?

    进行的count++操作,底层是三条指令在CPU上完成的!!!

    1. 把内存的数据读取到CPU寄存器中 load
    2. 把CPU的寄存器中的值进行 + 1 add
    3. 把寄存器中的值,写回内存中 save

    由于当前是两个线程修改一个变量,并且每次修改时三个步骤(不是原子的),还线程之间的调度顺序不确定,因此两个线程在真正执行这些操作的时候,就可能有多种执行的排列顺序!!

    在这里插入图片描述在这里插入图片描述
    这只是举出来4个,其实还有非常多的情况…
    在这些情况中,有些排列组合是没有问题的!
    但还有些排列组合是存在问题的…
    比如我举的例子,只有我画出来的前两种是没有问题的,其他情况都是有问题的!

    就一个例子图解一下为什么其他情况有问题:

    在这里插入图片描述
    在形如这样的排列顺序下,此时多线程自增就会存在"线程安全问题"!!

    💌

    整个线程调度过程中,执行的顺序都是随机的~~ 由于在调度过程中,出现"串行执行"两种情况的次数,和其他情况的次数,不确定
    因此得到的结果就是不确定的值~

    虽然结果不确定,但可以知道结果的范围:

    极端情况下:
    如果两个线程之间的调度全是 串行执行,结果是10w
    最多不会超过10w

    解决上述线程不安全问题,就可以在count++之前先加锁,在count++之后再解锁

    在加锁和解锁之间,别的线程无法修改!(别的线程只能阻塞等待,阻塞等待的线程状态,就是BLOCKED状态)

    在这里插入图片描述

    🚘线程不安全问题的解决方案:加锁

    🚘synchronized

    java代码中,进行加锁,使用synchronized关键字~

    synchronized几种写法:

    1. 修饰普通方法. 锁对象相当于this
    2. 修饰代码块,锁对象在()指定
    3. 修饰静态方法,锁对象相当于类对象 (不是锁整个类)

    🚞1.synchronized修饰一个普通方法

    这是最基本的使用~
    在这里插入图片描述当进入方法的时候,就会加锁,方法执行完毕自然解锁~

    在这里插入图片描述
    锁具有独占特性!
    如果当前锁没人来加,加锁操作就能成功!
    如果当前锁已经被人加了,加锁操作就会阻塞等待~
    在这里插入图片描述
    本来线程调度是随机的过程
    一旦两组load add save交织在一起就会产生线程安全问题!
    现在使用锁,就可以使两组load add save 能够串行执行了

    increase 里面涉及到锁竞争,这里的代码是串行执行的!
    但是for循环在加锁外面!俩for仍然是并发的~~
    在这里插入图片描述
    因此这个代码仍然比两个循环串行执行要快,但是肯定比不加锁要慢
    如果把for也写到加锁的代码里头,这个时候就和完全串行一样了!

    锁的代码范围不一样,对代码执行结果会有很大的影响!

    锁的代码越多,就叫做"锁的粒度越大/越粗"
    锁的代码越小,就叫做"锁的粒度越小/越细"

    在这里插入图片描述

    🚞2.synchronized修饰代码块

    1️⃣

    在这里插入图片描述

    synchronized 里面写的锁对象是this,谁调用increase,就是针对谁进行加锁~
    上面的代码就是在针对counter进行加锁! 并且两个线程针对同一个对象加锁,运行时会出现锁竞争(阻塞等待)

    2️⃣

    在这里插入图片描述
    上面的这个代码是俩线程在针对不同对象加锁,就不会出现锁竞争

    3️⃣

    在这里插入图片描述
    上面的代码是针对locker对象进行加锁~
    lockerCounter的一个普通成员(非静态成员),每个Counter实例中,都有自己的locker实例~
    上述代码中,counter对象是同一个,对应的counter里面的locker就是同一个对象,此时仍然是两个线程针对同一个对象加锁,仍然会存在锁竞争(阻塞等待)

    那1️⃣和3️⃣有什么区别呢?
    可以视为1️⃣和3️⃣从线程安全的角度来看待是没有任何区别的~

    4️⃣

    在这里插入图片描述
    也不会产生锁竞争,因为countercounter1不是同一个对象,里面的locker成员自然也不同,所以不会产生锁竞争

    5️⃣

    在这里插入图片描述
    但如果把locker改成静态成员(类属性),那么类属性是唯一的(一个过程中,类对象只有一个,类属性也是只有一份),这时虽然countercounter1是两个实例,但是这俩里面的locker其实是同一个locker,就会产生锁竞争!

    6️⃣

    在这里插入图片描述
    此处的锁对象,变成了一个“类对象”, 类对象在JVM进程中只有一个,如果多个线程来针对类对象进行加锁,那势必就会产生锁竞争!!
    上述代码实际上就是针对同一个Counter.class进行加锁,因此就会存在锁竞争

    锁竞争的目的是为了保证线程安全~

    🚞3.synchronized修饰静态方法

    锁的 SynchronizedDemo 类的对象

    public class SynchronizedDemo {
        public synchronized static void method() {
        
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    🚘总结

    在这里插入图片描述

    你可以叫我哒哒呀
    非常欢迎以及感谢友友们的指出问题和支持!
    本篇到此结束
    “莫愁千里路,自有到来风。”
    我们顶峰相见!
  • 相关阅读:
    Nvidia驱动卸载干净了,新驱动却还是安装不上?
    4.nodejs--nodejs简介、AJAX、MVC
    大学解惑10 - CSS中的content怎么换行,以及使用before伪类的优点
    API(3) StringBuffer类和StringBulider类
    2022-8-25 第七小组 学习日记 (day49)网上点餐系统
    Mysql——》Innodb存储引擎的索引
    Linux下大文件切割与合并
    通俗讲解傅里叶变换
    产品思维训练 | 亚马逊流量7-8月网站访客流量下降,请分析原因
    【Python实战】--输出与输出(updating)
  • 原文地址:https://blog.csdn.net/m0_58437435/article/details/126839444