2023.10.10 关于 线程安全 问题

news2024/10/5 5:24:21

目录

线程安全问题实例一

引发线程安全的原因

抢占式执行

多线程修改同一变量

操作的原子性

指令重排序

内存可见性问题

线程安全问题实例二

如何解决上述线程安全问题 

volatile 关键字

Java 内存模型 JMM(Java Memory Model)


线程安全问题实例一

class Counter {
    public int count = 0;
    
    public void add() {
        count++;
    }
}

public class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
//        搞两个线程,这两个线程分别针对 counter 来调用 5w 次的 add 方法
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
//        启动线程
        t1.start();
        t2.start();
//        等待两个线程结束
        t1.join();
        t2.join();
//        打印最终的 count 值
        System.out.println("count = " + counter.count);
    }
}

运行结果:

  • 我们通过两个线程各执行 5000 次 count 自增操作,count 的理想结果应为 100000,但是运行结果却相差甚大
  • 我们运行两次该代码,发现两次运行的结果也不同

了解 count++ 操作

  • 该操作本质上要分成三步
  • 先把内存中的值,读取到 CPU 寄存器中(load)
  • 再把 CPU 寄存器里的数值进行 +1 运算(add)
  • 最后把得到的结果写回到内存中(save)

引发线程安全的原因

抢占式执行

  • 多线程的调度是随机且毫无规律的
  • 抢占式执行是线程不安全的主要原因

多线程修改同一变量

  • 依据开头实例,两个线程并发执行对同一变量进行自增 5000 的操作,运行结果与期望值不符

  • 出现问题的关键是线程t1 和线程t2 的 load 指令
  • 两个线程 load 的 count 值均为对方修改 count 之后的值,此时是安全的,否则不安全

补充:

  • String 是不可变对象,其天然就是线程安全的
  • erlang 这个编程语言,其语法中就不存在 变量 这一概念,所有的数据都是不可变的,这样的语言更适合并发编程,其出现线程安全问题的概率大大降低

操作的原子性

  • 针对解决线程安全问题,从操作原子性入手是主要的手段
  • 原子为不可被拆分的基本单位
  • count++ 操作分为三个 CPU 指令,像 load、add、save 这样的 CPU 执行指令符合原子性的特点
  • 也正是因为 count++ 操作不是原子性的,从而会导致线程不安全的情况
  • 但是如果将 count++ 操作的三个CPU指令,包装成一个原子操作,这三个要么全部一起执行,要么不执行,在执行这三个指令时,CPU不能调度执行其他指令,从而就能很好的解决上述实例所出现的问题

指令重排序

  • 本质是编译器优化出现 bug
  • 编译器会根据你写的代码,在保持逻辑不变的前提下,进行相应的优化,调整代码的执行顺序,从而加快程序的执行效率

内存可见性问题

  • 指一个线程在使用对象状态时另一个线程在同时修改该状态
  • 我们需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化
  • 如果看不到修改后的变化,便会出现安全问题

总结:

  • 以上五种为典型原因,并不是全部原因
  • 一个代码的线程安全与否,主要应该具体对其进行分析,不能一概而论
  • 运行多线程代码,只要其没有 bug,就是安全的

线程安全问题实例二

  • 该实例基于 指令重排序 和 内存可见性问题
import java.util.Scanner;

class Test {
    public int count = 0;
}

public class ThreadDemo14 {
    public static void main(String[] args) {
        Test test = new Test();

        Thread t1 = new Thread(() -> {
                while (test.count == 0) {
                }
        });

        Thread t2 = new Thread(() -> {
                System.out.println("请输入一个数字,改变 count 值");
                Scanner scanner = new Scanner(System.in);
                test.count = scanner.nextInt();
        });

        t1.start();
        t2.start();
    }
}

运行结果:


代码整体逻辑:

  • 线程t1 的工作内容是通过 while 循环快速且不断对 count 值进行读取并与 0 进行大小比较
  • 线程t2 的工作内容是读取控制台输入的数字 1,并将其赋值给 count 变量
  • 预期结果:当线程t2 将 count 值改变时,此时线程t1 读取到 count != 0 ,从而能够直接结束 while 循环,线程t1 和线程t2 均运行完成,程序停止运行
  • 实际结果:线程t2 将 count 值改为 1 后,程序仍未停止,说明线程t1 并未结束 while 循环

预期结果与实际结果不一致原因:

  • 线程t1 的 while(test.count == 0) 分为两个步骤
  • 从内存中读取 count 的值到寄存器中(load 指令)
  • 在寄存器中的 count 与 0 进行值比较(cmp 指令)
  • 因为 while 内无额外逻辑代码,所以这两个指令会十分快速的循环执行
  • CPU 读写数据最快,内存次之,硬盘最慢,且他们之间均相差 3~4个数量级
  • 所以相比 load 指令要不断从内存中读取数据,cmp 指令直接在 CPU 上进行执行就要慢了很多很多
  • 编译器快速频繁的 load 读取 count  值,且多次 load 的 count 值还是一样的
  • 因为一般没有人能修改该代码,所以此时编译器就会认为反正读到的结果都是固定的,直接将代码优化为仅读取一次 count 值,此时代码的效率就会显著提高
  • 这时我们的线程t2 读取控制台输入的数字 1  并赋值给了 count 
  • 但是因为编译器将 while(test.count == 0) 代码优化成了仅读取一次 count 值,所以程序并不会因为 线程t2 将 count 值 修改为了 1 从而结束循环、结束程序执行
  • 从而上述是一个典型的 内存可见性问题 和 指令重排序问题(编译器优化问题)

总结:

  • 编译器优化在多线程情况下可能存在误判的情况

如何解决上述线程安全问题 

对于实例一

  • 为了将 count++ 操作的三个指令包装成一个原子操作,我们可以进行加锁操作
  • 使用 synchronized 关键字来修饰普通方法 add ,当执行进入该方法时,就会加锁,直到该方法执行完毕,就会解锁

  • 如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(BLOCKED)一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功

  •  synchronized 关键字的引入,每次执行 add 方法时都多了加锁和解锁的操作,有原来的 并发执行 转变为 串行执行,从而减慢了执行效率,但是保证了线程的安全性
  • 所以我们需要根据需求进行分析取舍,只追求校率,不再乎准确率,可以不加锁,如果以准确率为前提条件,加锁操作就显得十分有必要了

修改后运行结果

注意:

  • 在加锁区间(lock -> unlock 区间)中,CPU 不是一定要一口气执行完,中间也是可以有调度切换的,即使执行到一半 CPU 调度切换执行其他,当其余线程要想获取该方法时,还是会被阻塞(BOLCKED),无法获取该方法
  • 虽然加锁之后,代码执行效率降低了,但是还是要比单线程执行要快
  • 因为加锁仅针对 count++ 加锁,但除了 count++ 外还有 for 循环代码,for循环代码可以并发执行,只是 count++ 变为串行执行,还是要比单线程全串行执行要快

对于实例二

volatile 关键字

  • volatile 关键字有两大作用
  • 禁止指令重排序:保证指令执行的顺序,防止编译器优化而修改指令执行顺序,引发线程安全问题
  • 保证内存可见性:保证了读取到的数据时内存中的数据,而不是缓存,简单来说就是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值

Java 内存模型 JMM(Java Memory Model)

  • JMM 定义了Java 程序中多线程并发访问共享内存(主存)的行为规范
  • volatile 关键字禁止了编译器优化,避免了直接读取 CPU 寄存器中缓存的数据,而是每次重新读内存
  • 站在 JMM 角度看 volatile
  • 正常程序执行过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理
  • 编译器优化可能会导致不是每次都真的取读取主内存,而直接读取工作内存中的缓存数据(导致内存可见性问题)
  • 而 volatile 的作用就是保证每次读取内存都是真的从主存中重写读取

修改后运行结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1085459.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决echarts配置滚动(dataZoom)后导出图片数据不全问题

先展现一个echarts&#xff0c;并配置dataZoom&#xff0c;每页最多10条数据&#xff0c;超出滚动 <div class"echartsBox" id"echartsBox"></div>onMounted(() > {nextTick(() > {var chartDom document.getElementById(echartsBox);…

前端URL拼接路径参数

前端URL拼接路径参数 一、应用场景二、具体实现1.字符串拼接2.URL对象实现 四、完整代码 一、应用场景 我们有时候会遇到浏览器URL拼接参数的场景&#xff0c;例如页面跳转时&#xff0c;带上一个特定的标识&#xff1a;https://www.baidu.com?fromcsdn 二、具体实现 1.字符…

微软发布2023年10月补丁,修复了103个缺陷,包括2个活跃的漏洞利用

导语 最近&#xff0c;微软发布了2023年10月的补丁更新&#xff0c;共修复了103个缺陷。这些补丁包括两个正在被黑客利用的漏洞。让我们来看看这些补丁的具体内容和影响。 修复103个缺陷 微软此次的补丁更新共修复了103个缺陷&#xff0c;其中13个被评为严重&#xff0c;90个被…

gradle对应jdk版本

官网地址-兼容性矩阵&#xff1a;Compatibility Matrix Gradle运行在所有主要的操作系统上。它需要Java开发工具包&#xff08;JDK&#xff09;版本8或更高版本才能运行。有关详细信息&#xff0c;您可以查看兼容性矩阵。

DailyPractice.2023.10.12

文章目录 1.[1. 两数之和]2.[49. 字母异位词分组]3.[128. 最长连续序列]4.[283. 移动零]5.[11. 盛最多水的容器]6.[15. 三数之和]7.[3. 无重复字符的最长子串]8.[206. 反转链表]9.[141. 环形链表]10.[160. 相交链表] 1.[1. 两数之和] 1. 两数之和 class Solution { public:ve…

单目标优化算法:火鹰优化算法(Fire Hawk Optimizer,FHO)求解23个函数--提供MATLAB代码

一、火鹰优化算法FHO 火鹰优化算法&#xff08;Fire Hawk Optimizer&#xff0c;FHO&#xff09;由Mahdi Azizi等人于2022年提出&#xff0c;该算法性能高效&#xff0c;思路新颖。 单目标优化&#xff1a;火鹰优化算法&#xff08;Fire Hawk Optimizer&#xff0c;FHO&#…

使用Tortoisegit界面拉起master主分支以副分支以及命令行操作

文章目录 1、Gui操作界面2、命令行操作 1、Gui操作界面 "小乌龟"通常指的是Git的图形用户界面&#xff08;GUI&#xff09;工具&#xff0c;其中比较常见的是TortoiseGit。下面是使用TortoiseGit来拉取&#xff08;checkout&#xff09;一个Git分支的步骤&#xff1a…

数字时代的自我呈现:探索个人形象打造的创新工具——FaceChain深度学习模型工具

数字时代的自我呈现&#xff1a;探索个人形象打造的创新工具——FaceChain深度学习模型工具 1.介绍 FaceChain是一个可以用来打造个人数字形象的深度学习模型工具。用户仅需要提供最低一张照片即可获得独属于自己的个人形象数字替身。FaceChain支持在gradio的界面中使用模型训…

机器学习(22)---信息熵、纯度、条件熵、信息增益

文章目录 1、信息熵2、信息增益3、例题分析 1、信息熵 1. 信息熵(information entropy)是度量样本集合纯度最常用的一种指标。信息的混乱程度越大&#xff0c;不确定性越大&#xff0c;信息熵越大&#xff1b;对于纯度&#xff0c;就是信息熵越大&#xff0c;纯度越低。 2. 纯度…

CSI2与CDPHY学习

注意&#xff1a;本文是基于CSI2-V3.0 spec。 其中CPHY为 V2.0 DPHY为V2.5 本文主要在packet级别介绍CSI2与对应的CDPHY&#xff0c;需要注意的是CDPHY的burst数据就是以packet为单位 1.CSI-CPHY 1.1CPHY的多lane分配与合并 csi-cphy规定至少需要一条lane用于传输视频&am…

ubuntu20.04 nerf Instant-ngp (下) 复现,自建数据集,导出mesh

参考链接 Ubuntu20.04复现instant-ngp&#xff0c;自建数据集&#xff0c;导出mesh_XINYU W的博客-CSDN博客 GitHub - NVlabs/instant-ngp: Instant neural graphics primitives: lightning fast NeRF and more youtube上的一个博主自建数据集 https://www.youtube.com/watch…

C++菜鸟日记1

共用体&#xff1a; #include<iostream> using namespace std; int main() {struct widge{char brand[20];int type;union id{long id_num;char id_char[20];}id_val;};widge prize;cout << "Please cin the prize.type mumber:" << endl;cin >…

01Linux基础

附件:day26–linux入门.pdf Linux是 基于Unix 的开源免费的操作系统&#xff0c;由于系统的稳定性和安全性几乎成为程序代码运行的最佳系统环境。 &#xff08;程序基本上在Linux上发布&#xff09; Linux系统的应用非常广泛&#xff0c;不仅可以长时间的运行我们编写的程序代…

【C++14算法】make_unique

文章目录 前言一、make_unique函数1.1 什么是make_unique?1.2 如何使用make_unique?1.3 make_unique的函数原型如下&#xff1a;1.4 示例代码示例1: 创建一个动态分配的整数对象示例2: 创建一个动态分配的自定义类型对象示例3: 创建一个动态分配的数组对象示例4: 创建一个动态…

STM32 CubeMX PWM三种模式(互补,死区互补,普通)(HAL库)

STM32 CubeMX PWM两种模式&#xff08;HAL库&#xff09; STM32 CubeMX STM32 CubeMX PWM两种模式&#xff08;HAL库&#xff09;一、互补对称输出STM32 CubeMX设置代码部分 二、带死区互补模式STM32 CubeMX设置代码 三、普通模式STM32 CubeMX设置代码部分 总结 一、互补对称输…

2023.10.12

#include <iostream>using namespace std; //定义动物类 class Animal { private:string name; public:Animal(){}Animal(string name):name(name){}~Animal(){}//定义虚函数virtual void perform()0;//表演的节目void show(){cout << "Please enjoy the spec…

平衡二叉树(AVL) 的认识与实现

文章目录 1 基本1.1 概念1.2 特点1.3 构建1.4 调整1.4.1 RR1.4.1.1 示例1.4.1.2 多棵树不平衡 1.4.2 LL1.4.2.1 示例 1.4.3 LR1.4.3.1 示例 1.4.4 RL1.4.4.1 示例 1.5 实现1.5.1 示例1.5.2 完善 1 基本 1.1 概念 平衡二叉树是一棵合理的二叉排序树 解释 对于这么一个序列 如…

2023 | github无法访问或速度慢的问题解决方案

github无法访问或速度慢的问题解决方案 前言: 最近经常遇到github无法访问, 或者访问特别慢的问题, 在搜索了一圈解决方案后, 有些不再有效了, 但是其中有几个还特别好用, 总结一下. 首选方案 直接在github.com的域名上加一个fast > githubfast.com, 访问的是与github完全相…

03-RocketMQ高级原理

目录汇总&#xff1a;RocketMQ从入门到精通汇总 上一篇&#xff1a;02-RocketMQ开发模型 前面的部分我们都是为了快速的体验RocketMQ的搭建和使用。这一部分&#xff0c;我们慢下来&#xff0c;总结并学习下RocketMQ底层的一些概念以及原理&#xff0c;为后面的深入学习做准备。…

使用宝塔面板在Linux上搭建网站,并通过内网穿透实现公网访问

文章目录 前言1. 环境安装2. 安装cpolar内网穿透3. 内网穿透4. 固定http地址5. 配置二级子域名6. 创建一个测试页面 前言 宝塔面板作为简单好用的服务器运维管理面板&#xff0c;它支持Linux/Windows系统&#xff0c;我们可用它来一键配置LAMP/LNMP环境、网站、数据库、FTP等&…