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

2023-12-05

线程安全

线程安全的定义

线程安全:某个代码无论是在单线程上运行还是在多线程上运行,都不会产生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次就不需要从内存中读了,效率就大大提高了.

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

贵!

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

线程安全的问题以及解决方案 的相关文章

随机推荐

  • 加快10倍!GPT-Fast来了!使用原生 PyTorch 加速生成式 AI

    点击下方 卡片 关注 CVer 公众号 AI CV重磅干货 第一时间送达 点击进入 gt 视觉和Transformer 微信交流群 扫码加入CVer知识星球 可以最快学习到最新顶会顶刊上的论文idea 和CV从入门到精通资料 以及最前沿项目
  • 关于#c++#的问题:将输入的字符串a复制给字符串b,然后用数组名a和b输出两个字符串(相关搜索:指针变量)

    关于 c 的问题 将输入的字符串a复制给字符串b 然后用数组名a和b输出两个字符串 相关搜索 指针变量 用指针变量实现 将输入的字符串a复制给字符串b 然后用数组名a和b输出两个字符串 输入样例 i am the best 输出样例 字符串
  • 什么是ABA问题?怎么解决?

    具体来说 ABA问题可以分为以下几个步骤 初始状态 假设数据的初始值为A 事务T1读取数据 事务T1读取数据的值为A 事务T2修改数据 事务T2将数据的值从A修改为B 然后再修改回A 即先经过了A gt B gt A的过程 事务T1继续操作
  • MySQL 有哪些重要的日志文件?(错误日志、查询日志、慢日志、redolog、undolog、binlog)

    MySQL 中的重要日志分为以下几个 错误日志 用来记录 MySQL 服务器运行过程中的错误信息 比如 无法加载 MySQL 数据库的数据文件 或权限不正确等都会被记录在此 还有复制环境下 从服务器进程的信息也会被记录进错误日志 默认情况下
  • 【Python从入门到进阶】43.验证码识别工具结合requests的使用

    接上篇 42 使用requests的Cookie登录古诗文网站 上一篇我们介绍了如何利用requests的Cookie登录古诗文网 本篇我们来学习如何使用验证码识别工具进行登录验证的自动识别 一 图片验证码识别过程及手段 上一篇我们通过re
  • 拒绝写重复代码,试试这套开源Spring Boot组件,效率翻倍!

    1 简介 Graceful Response是一个Spring Boot技术栈下的优雅响应处理器 提供一站式统一返回值封装 全局异常处理 自定义异常错误码 等功能 使用Graceful Response进行web接口开发不仅可以节省大量的时
  • android 13.0 去掉usb授权提示框 默认给予权限

    1 概述 在13 0的系统rom产品开发中 在进行iot关于插入usb设备的开发过程中 在插入usb设备时会弹出usb授权提示框 也带来一些不便 这个需要默认授予USB权限 插拔usb都不弹出usb弹窗所以这要从usb授权相关管理页默认给与
  • uniapp打包的h5项目多了接口调用https://api.next.bspapp.com/client

    产生跨域问题 这个实际上是因为该项目在manifest json文件中勾选了 uni统计配置 导致的 取消勾选就可以了 如果是小程序项目 在小程序开发者工具中添加可信任域名就可以了 可以看看下面这个链接内容 uni app H5跨域问题解决
  • 【go语言开发】编写单元测试

    本文主要介绍使用go语言编写单元测试用例 首先介绍如何编写单元测试 然后介绍基本命令的使用 最后给出demo示例 文章目录 前言 命令 示例 前言 在go语言中编写单元测试时 使用说明 测试文件命名 在 Go 语言中 测试文件的命名应与被测
  • RestTemplate

    一 RestTemplate是什么 RestTemplate是spring提供的Http协议实现类 也就是说导入spring boot starter web的项目可以直接使用RestTemplate类 就是基于模板方法设计模式的 封装了所
  • 租用高防服务器得必要

    租用高防服务器得必要 一 高防服务器的防御性 在网络恶意暴增的情况下 如何避免DDOS和CC 可以说是企业最为关注的问题 而防御因网络出现不整出运转等问题 最后就是选择高防服务器 高防服务器对于维护独立服务器的稳定性和拓展流量都有很大的帮助
  • 8-1运用指针比较三个数的大小

    include
  • springboot——helloworld入门

    springboot 简化spring开发 约定大于配置 提供完成restful的框架 注解 配置等完成 restful restful就是提供一堆标准的方法 例如get put等完成http的网站操作 helloworld入门 注解 Sp
  • windows下bitsandbytes安装报错解决

    RuntimeError CUDA Setup failed despite GPU being available Please run the following command to get more information pyth
  • c++ 构造

    include
  • ssh的实验室预约系统Python项目PHP程序Java安卓APP设计asp.net微信小程序

    文末获取联系方式 我们的毕设辅导团队由一群经验丰富 专业素质过硬的导师组成 他们来自于各个领域的专业人士 具备丰富的实践经验和深厚的学术背景 无论你的毕设是关于Python Java 小程序 asp net PHP nodejs还是其他领域
  • Matlab 生成license

    参考下面两个帖子 https ww2 mathworks cn matlabcentral answers 389888 matlab https www mathworks com matlabcentral answers 131749
  • 机器学习笔记 - 什么是模型量化压缩技术?

    一 简述 我们都知道现实世界是连续的状态 而计算机世界是离散的状态 这是什么意思呢 我们看一下下图 最右边的马力欧 高清 的状态 可以想象现实世界是连续的状态 而电脑世界在图像上呈现的是一格一格子的状态 左图 是离散的状态 所以在计算机世界
  • unordered_set unordered_multiset

    unordered set 名字 描述 insert 插入一个新元素 begin end 返回一个迭代器 指向第一个元素 最后一个元素后的理论元素 count 计算在无序集合容器中特定元素的出现次数 find 搜索元素 clear
  • 线程安全的问题以及解决方案

    线程安全 线程安全的定义 线程安全 某个代码无论是在单线程上运行还是在多线程上运行 都不会产生bug 线程不安全 单线程上运行正常 多线程上运行会产生bug 观察线程不安全 看看下面的代码 public class ThreadTest1