当前位置: 首页 > article >正文

线程安全的问题以及解决方案

线程安全

线程安全的定义

线程安全:某个代码无论是在单线程上运行还是在多线程上运行,都不会产生bug.

线程不安全:单线程上运行正常,多线程上运行会产生bug.

观察线程不安全

看看下面的代码:

public class ThreadTest1 {
    public static int count = 0;

    public static void main(String[] args)throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 10000; i++) {
                count++;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();

        System.out.println(count);
    }
}

按照常理来讲,运行的结果应该是20000,但让我们来看看实际的运行结果: 

显然结果与我们预期的不一致,但为什么会出现这种问题呢?

让我们来看一下线程不安全的原因:

线程不安全的原因

重点:线程调度是随机的 

1.根本原因:操作系统上线程是"抢占式执行",而且是"随机调度"的,执行顺序会有很多变数.(罪魁祸首)

2.代码结构:多个线程同时修改一个变量(1. 一个线程修改一个变量(没事) 2.多个线程同时读取一个变量(没事) 3.多个线程同时修改不同的变量(没事))

3.直接原因:上述线程的修改操作本身不是"原子的",比如count++这条语句,它本身包含多个cpu指令(这个例子后面会详细讲).执行了一半可能会调度走.

4.内存可见性问题(例子里的代码还没有),后面的文章会讲.

5.指令重排序问题

分析例子代码中的问题

这个问题就主要出现在count++这条语句中.它本身包含这些cpu指令:LOAD,ADD,SAVE

让我们回顾一下这几条指令的含义:

(1)load:从内存中读取数据到cpu的寄存器

(2)add:把寄存器中的值+1,

(3)save:把寄存器的值写回到内存中.

因此count++这条语句的执行的流程如下:

这是一个count++的执行流程,但是在多进程程序中,这三条指令一定会连贯执行吗(规范的按照一个load->add->save执行)? ,留着这个问题,来看看后面的内容:

修改共享数据

在例子中,显然是符合多个线程修改同一个变量的.

上面线程不安全的代码中,涉及到多个线程对count变量进行的修改.

此时这个count是一个多线程都能访问到的共享数据,因此t1和t2都可以对count进行修改.

原子性

什么是原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人.如果没有任何机制保证,A进入房间之后,还没有出来;B是不是也可以进入房间,打断A在房间中的隐私.这个就是不具备原子性的.

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其它的线程插进来了,如果这个操作被打断了,结果可能就是错误的.

这点也和线程的抢占式调度密切相关,如果线程不是"抢占"的,就算没有原子性,也问题不大.

综合以上,我们可以得到引起问题的原因:共享数据的修改以及数据并非原子的.

通过下面这个图就可以看出来:

等等还有很多种执行顺序(无数种).

比如图二:由于t2的load抢占在t1的add前执行,因此导入时count值都一样,那么执行的结果最后就是+1,而不是理想中的各自线程都给count+1,最后执行完两个就是+2了.那么有没有一种情况执行结果是正常的,当然有:

类似这种每个线程执行时,三条指令都是在一块的,这种运行是正确的,那么有没有一种方法能按照这样运行呢?有的.

只要将count++操作上锁,使得这三条一起指令执行完之后,才会执行下一个操作.

 有时也把这个现象叫做同步互斥,表示操作是互相排斥的.

解决上面的问题
public class ThreadTest {
    public static final Object locker = new Object();
    public static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                //进入大括号会上锁
                synchronized (locker) {
                    count++;
                }//出大括号会解锁
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

这里用到的机制(synchronized)后面的文章会解释. 

可见性

可见性指,一个线程对共享变量值的修改,能够及时被其它线程看到.

Java内存模型(JMM) :Java虚拟机规范中定义了java内存模型.

目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

线程之间的共享变量存在主内存(可以看作为上面的内存)

每一个线程都有自己的工作内存(并不是真正的内存,可以看作为上面的cpu寄存器(也有可能是cpu缓存,不过都差不多))

当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存种读取数据.

当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存.

(1)初始情况下,两个线程的工作内存一致

(2)一旦线程1修改了a的值,此时主内存并不一定可以及时同步过来(是在寄存器中改动的,因为寄存器比较快) 

此时引入了一个问题:

为什么要在主内存和工作内存种麻烦的拷来拷去?

因为CPU访问自身寄存器的速度以及高速缓存的速度,远远超过访问内存的速度(快了几千至上万倍) 


比如某个代码种要连续10次读取某个变量的值,如果10次都从内存中度,速度是很慢的.但如果只是第一次从内存中读,读到的结果缓存到CPU某个寄存器中,那么后面9次就不需要从内存中读了,效率就大大提高了.

那么问题又来了,既然寄存器速度这么快,还要内存干嘛?

贵!

后面我们将用更详细的方法解决线程安全问题,敬请期待.


http://www.kler.cn/a/155012.html

相关文章:

  • Taro React-Native IOS 打包发布
  • 在 WPF 中,如何实现数据的双向绑定?
  • 【Qt聊天室客户端】消息功能--发布程序
  • 「QT」文件类 之 QDataStream 数据流类
  • 数据结构Python版
  • 算法训练(leetcode)二刷第二十六天 | *452. 用最少数量的箭引爆气球、435. 无重叠区间、*763. 划分字母区间
  • 【重点】【双指针】15. 三数之和
  • Vue diff 算法探秘:如何实现快速渲染
  • Gson的用法详解
  • 中兴小鲜50 ZTE 畅行50 刷机救砖演示机7543n root 虎贲 展锐 T760 解锁BL
  • 人工智能 - 人脸识别:发展历史、技术全解与实战
  • 开源免费跨平台数据同步工具-Syncthing
  • Unity3D URP 自定义范围的特效热扭曲详解
  • LLM:《第 3 部分》从数学角度评估封闭式LLM的泛化能力
  • 安全SCDN对网站蜘蛛抓取有影响吗,使用SCDN对百度蜘蛛抓取有否好处
  • CentOS7 网络配置
  • Linux的权限(一)
  • Ubuntu22.04无需命令行安装中文输入法
  • C++生成静态库和动态库
  • 智慧用电安全动态监控系统
  • centos7-docker安装与使用
  • 网络虚拟化场景下网络包的发送过程
  • C/C++---------------LeetCode第35. 搜索插入位置
  • C++ day48 打家劫舍
  • 数学建模之典型相关分析
  • Redis--10--Pipeline