多线程之旅:线程安全问题

news2025/1/22 14:54:39

之前说到了多线程的创建和一些属性等等,接下来,就来讲讲多线程安全问题。

小编引入这段代码讲解下:
 

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

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

代码内容就是,两个线程分别负责计算5000次累加运算,最后得到的结果是10000

三次运行结果如下:

为什么会这样子呢?

这里就涉及到了线程安全问题。

那么小编来讲讲为什么出现这样子吧。

首先,我们得知道线程调度是不确定性,即count++这个操作具有被篡改的可能性

那么为什么又有被篡改的可能性呢?

这是因为count++操作,不是原子性的?

问题又来了,什么又是原子性操作呢?

原子性操作:即是一个操作过程中,执行期间不会被打断,要么全部执行完成,要么全部不执行。

不存在执行一半就不执行了。

回到上一个问题,count++为什么不是原子性操作呢?

因为count++这个过程,在CPU内部操作,是分成了三个指令操作执行。

1.把内存中的数据,读取到CPU的寄存器中   load

2.把CPU寄存器里的数据+1                            add

3.把寄存器里的值写回到内存中                      save

当然,值得注意的是,load、add、save三个CPU指令,其表示的英文由于不同架构的CPU,所以有着不同的指令集,所以名称也会有所不同。

比如

ARM 架构

  • LDR 用于加载数据。

  • ADD 用于加法运算。

  • STR 用于存储数据。

这里load、add、save是计算机操作基础,几乎所有的cpu支持此类操作。

三个指令的操作过程

再回到上一个问题

为什么会被篡改呢?

我们可以得知,两个线程中,均有count++操作

也就是说两个线程中执行到count++,都会执行三个指令操作。

此时这个三个指令操作不是原子性的,并且线程的调度是随机的。

即有这样的一种情况:

当线程1执行到load操作的时候,线程2就被调度了,线程1没有被调度,

此时线程2执行load save add一系列操作,此时内存中count的值=1,如何线程1调度回来,执行add、save,那么此时就会出现count++,只被加了一次的情况

值得说明的是,当时两个线程的load的值还是0

图解如下:

所以由于线程调度的不确定性,导致load、add、save,不确定顺序执行情况有多种。

如若按照t1线程load、add、save后,此时内存中count的值变为1

t2之后也是load、add、save执行,此时内存中count的值变为2

当然,两者调换过来也是一样的的

这样才是执行正确了。

即如图下:

到这里,我们就解释了,为什么这个不能得到10000这个结果了

“罪魁祸首”:还是这个线程调度的不确定性。

同时我们发现,这个count变量被两个线程同时进行修改了,所以这也是出现线程安全问题之一。

出现安全问题的原因有很多的,小编接着带大家看看。

代码实例:

public class Demo16 {
    public  static int n=0;

    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (n==0){
            
            }
            System.out.println("t1循环结束");
        });
        Thread t2=new Thread(()->{
            System.out.println("请输入n的值");
            Scanner scanner=new Scanner(System.in);
            n=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

运行结果

可以看到,按照正常的逻辑看待,如若n被控制台输入不同的值,此时这个t1循环结束就会被打印。

那么为什么会出现这样的问题?

这里就涉及到了内存可见性问题。

那什么又是内存可见性问题呢?

以上面的例子来举例

t1线程中while条件,判断n是否等于0,那么这个操作呢

操作1.先从内存读取到寄存器中

操作2.通过比较指令,比较寄存器和0的值(这个操作执行的非常快)

相比之下,从内存中读取数据到寄存器中的操作,显得会变得很慢。

此时jvm执行代码的时候发现,

由于执行速度较快,在用户未修改n的值的时候,就是这段时间内,每次进行循环执行操作1,使得执行开销较大,所以jvm变得“胆大”,直接把操作1进行优化掉了。

此时就是不会再读取内存中的值,即读取寄存器中的值。这个操作使得执行操作开销变小了

但是,t2线程中,当用户执行输入的时候,n的值改变,这个值是写回到内存中的,t1线程读取n的值,是在寄存器中了,很明显,位置不一样,t1线程感知不到n的变化,那么就无法结束while循环。

所以,这就是内存可见性问题。(一个线程读,一个线程修改)

那为什么呢编译器会做出这个优化操作呢?

本质上也是为了代码编写的更加高效,因为不是每个程序员都能写出效率很高的代码,

所以即使你代码写的一般,也不会落后很多的执行速度。

值得注意的是,这样的优化是基于原有逻辑不变的基础上的。

解决办法:
在n变量加个volatile关键字

volatile:易变的

即告诉编译器这个变量是易变,不要随意优化它,确保这每次循环都能从内存重新读取数据。

代码修正:

    public volatile static int n=0;

当然,还有个出现线程安全的例子:

代码实例:
 

public class Demo22 {
    private static boolean flag = false;
    private static int number = 0;

    public static void main(String[] args) throws InterruptedException {
        // 线程 1
        Thread t1 = new Thread(() -> {
            number = 42;           // 1
            flag = true;           // 2
        });

        // 线程 2
        Thread t2 = new Thread(() -> {
            if (flag) {            // 3
                System.out.println("Number: " + number);  // 4
            }
        });

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

        t1.join();
        t2.join();
    }
}

代码确实按道理来说也打印了42了,那为什么小编还说这是有问题呢?

我们之前说过线程的调度是具有不确定性的,所以执行的结果如若没有锁的加持下,结果也是不唯一。

那么它的另一种结果会是输出number:0

出现的原因就是指令重排序了。

那么又是指令重排序呢?

在这个例子中,t1线程中

number = 42;

 flag = true;

这两个是独立操作,即变量中,没有依赖关系,那么此时,编译器有可能把flag = true,放到numbe = 42之前执行,此时很显然,如若出现这样的情况,那么t2线程中的if语句就会为真,执行打印代码

但是此时的42没有赋值到number中,那么就有可能打印0的值。

所以这就是指令重排序原因。

那为什么运行结果没有出现0呢?

这是跟jvm和操作系统调度,编译器优化等等原因有关。

解决上面的办法可以通过锁或者volatile关键字来进行处理。

ok,那么来总结下内存可见性和指令重排序

内存可见性:内存可见性是指在一个线程对共享变量进行修改后,这些修改对于其他线程是立即可见的。

指令重排序:指令重排序是指编译器或处理器为了优化性能而改变程序中指令的实际执行顺序。

最后总的来说,出现线程安全问题的原因

1.操作系统,针对线程的调度,是具有不确定性(抢占型执行)(根本原因)
2.代码结构,即多个线程同时修改一个变量

3.修改操作不是原子性的。

4.内存可见性

5.指令重排序

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

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

相关文章

html学习笔记(3)

一、文本格式标签 效果标签&#xff08;旧版&#xff09;标签&#xff08;语义化&#xff0c;强调&#xff09;加粗<b><strong>倾斜<i><em>下划线<u><ins>删除线<s><del> 前面的标签 b 、 i 、 u 、 s 就仅仅是实现加粗、倾…

Postgresql源码(141)JIT系列分析汇总

JIT的东西比较零散&#xff0c;本篇对之前的一些列分析做个汇总、整理。 涉及&#xff1a; 《Postgresql源码&#xff08;113&#xff09;表达式JIT计算简单分析》 《Postgresql源码&#xff08;127&#xff09;投影ExecProject的表达式执行分析》 《Postgresql源码&#xff08…

Maven多环境打包方法配置

简单记录一下SpringBoot多环境打包配置方法&#xff0c;分部署环境和是否包含lib依赖包两个维度 目录 一、需求说明二、目录结构三、配置方案四、验证示例 一、需求说明 基于Spring Boot框架的项目分开发&#xff0c;测试&#xff0c;生产等编译部署环境&#xff08;每一个环境…

SDL2基本使用

前言 在这里记录SDL的环境基本搭建和使用&#xff0c;方便回忆。使用该图形库也是为了方便在没有单片机和显示模块的使用&#xff0c;也能对简单验证些关于图形构建或界面管理的猜想和测试&#xff0c;所以下述不会探讨过于深入的东西。当然&#xff0c;也可以通过SDL官网查看介…

【Linux系统编程】—— 从零开始实现一个简单的自定义Shell

文章目录 什么是自主shell命令行解释器&#xff1f;实现shell的基础认识全局变量的配置初始化环境变量实现内置命令&#xff08;如 cd 和 echo&#xff09;cd命令&#xff1a;echo命令&#xff1a; 构建命令行提示符获取并解析用户输入的命令执行内置命令与外部命令Shell的主循…

认识BOM

BOM 弹出层 可视窗口尺寸 屏幕宽高 浏览器内核和其操作系统的版本 剪贴板 是否允许使用cookie 语言 是否在线

[c语言日寄]结构体的使用及其拓展

【作者主页】siy2333 【专栏介绍】⌈c语言日寄⌋&#xff1a;这是一个专注于C语言刷题的专栏&#xff0c;精选题目&#xff0c;搭配详细题解、拓展算法。从基础语法到复杂算法&#xff0c;题目涉及的知识点全面覆盖&#xff0c;助力你系统提升。无论你是初学者&#xff0c;还是…

Linux系统的第一个进程是什么?

Linux进程的生命周期从创建开始&#xff0c;直至终止&#xff0c;贯穿了一个进程的整个存在过程。我们可以通过系统调用fork()或vfork()来创建一个新的子进程&#xff0c;这标志着一个新进程的诞生。 实际上&#xff0c;Linux系统中的所有进程都是由其父进程创建的。 既然所有…

5. 马科维茨资产组合模型+AI金融智能体(qwen-max)识别政策意图方案(理论+Python实战)

目录 0. 承前1. AI金融智能体1.1 What is AI金融智能体1.2 Why is AI金融智能体1.3 How to AI金融智能体 2. 数据要素&计算流程2.1 参数集设置2.2 数据获取&预处理2.3 收益率计算2.4 因子构建与预期收益率计算2.5 协方差矩阵计算2.6 投资组合优化2.7 持仓筛选2.8 AI金融…

PostMan最新版本及离线安装指南

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;PostMan是一款流行的API测试工具&#xff0c;它提供了一个直观的用户界面&#xff0c;方便Web开发者和测试人员进行接口测试。本文将指导你如何安装最新版的PostMan&#xff0c;包括在线安装和离线安装两种方法。…

记录一次k8s起不来的排查过程

我在k8s集群&#xff0c;重启了一个node宿主机&#xff0c;竟然发现kubelet起不来了&#xff01;报错如下 这个报错很模糊&#xff0c;怎么排查呢。这样&#xff0c;开两个界面&#xff0c;一个重启kubelet&#xff0c;一个看系统日志(/var/log/message:centos&#xff0c;/va…

grafana + Prometheus + node_exporter搭建监控大屏

本文介绍生产系统监控大屏的搭建&#xff0c;比较实用也是实际应用比较多的方式&#xff0c;希望能够帮助大家对监控系统有一定的认识。 0、规划 grafana主要是展示和报警&#xff0c;Prometheus用于保存监控数据&#xff0c;node_exporter用于实时采集各个应用服务器的事实状…

2024年博客之星主题创作|从零到一:我的技术成长与创作之路

2024年博客之星主题创作&#xff5c;从零到一&#xff1a;我的技术成长与创作之路 个人简介个人主页个人成就热门专栏 历程回顾初来CSDN&#xff1a;怀揣憧憬&#xff0c;开启创作之旅成长之路&#xff1a;从平凡到榜一的蜕变持续分享&#xff1a;打卡基地与成长复盘四年历程&a…

Golang的网络编程安全

Golang的网络编程安全 一、Golang网络编程的基本概念 作为一种现代化的编程语言&#xff0c;具有优秀的并发特性和网络编程能力。在Golang中&#xff0c;网络编程是非常常见的需求&#xff0c;可以用于开发各种类型的网络应用&#xff0c;比如Web服务、API服务、消息队列等。Go…

【2024年华为OD机试】(C/D卷,200分)- 5G网络建设 (JavaScriptJava PythonC/C++)

一、问题描述 题目描述 现需要在某城市进行5G网络建设&#xff0c;已经选取N个地点设置5G基站&#xff0c;编号固定为1到N。接下来需要各个基站之间使用光纤进行连接以确保基站能互联互通。不同基站之间假设光纤的成本各不相同&#xff0c;且有些节点之间已经存在光纤相连。 …

消息队列篇--原理篇--RabbitMQ和Kafka对比分析

RabbitMQ和Kafka是两种非常流行的消息队列系统&#xff0c;但它们的设计哲学、架构特点和适用场景存在显著差异。对比如下。 1、架构设计 RabbitMQ&#xff1a; 基AMQP协议&#xff1a;RabbitMQ是基于AMQP&#xff08;高级消息队列协议&#xff09;构建的&#xff0c;支持多…

玻璃样式的登录界面

AI越来越火了,我们想要不被淘汰就得主动拥抱。推荐一个人工智能学习网站,通俗易懂,风趣幽默,最重要的屌图甚多,忍不住分享一下给大家。点击跳转到网站 先看样式: 源码: <div class="wrapper">

Python数据可视化(够用版):懂基础 + 专业的图表抛给Tableau等专业绘图工具

我先说说文章标题中的“够用版”啥意思&#xff0c;为什么这么写。 按照我个人观点&#xff0c;在使用Python进行数据分析时&#xff0c;我们有时候肯定要结合到图表去进行分析&#xff0c;去直观展现数据的规律和特定&#xff0c;那么我们肯定要做一些简单的可视化&#xff0…

物联网网关Web服务器--CGI开发实例BMI计算

本例子通一个计算体重指数的程序来演示Web服务器CGI开发。 硬件环境&#xff1a;飞腾派开发板&#xff08;国产E2000处理器&#xff09; 软件环境&#xff1a;飞腾派OS&#xff08;Phytium Pi OS&#xff09; 硬件平台参考另一篇博客&#xff1a;国产化ARM平台-飞腾派开发板…

HTML新春烟花

系列文章 序号目录1HTML满屏跳动的爱心&#xff08;可写字&#xff09;2HTML五彩缤纷的爱心3HTML满屏漂浮爱心4HTML情人节快乐5HTML蓝色爱心射线6HTML跳动的爱心&#xff08;简易版&#xff09;7HTML粒子爱心8HTML蓝色动态爱心9HTML跳动的爱心&#xff08;双心版&#xff09;1…