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

news2025/1/23 22:39:37

线程安全

线程安全的定义

线程安全:某个代码无论是在单线程上运行还是在多线程上运行,都不会产生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.coloradmin.cn/o/1279475.html

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

相关文章

<JavaEE> synchronized关键字和锁机制 -- 锁的特点、锁的使用、锁竞争和死锁、死锁的解决方法

目录 一、synchronized 关键字简介 二、synchronized 的特点 -- 互斥 三、synchronized 的特点 -- 可重入 四、synchronized 的使用示例 4.1 修饰代码块 - 锁任意实例 4.2 修饰代码块 - 锁当前实例 4.3 修饰普通方法 - 锁方法所在实例 4.4 修饰代码块 - 锁指定类对象 …

threadlocal - 黑马程序员

目录 1、ThreadLocal介绍1.2 ThreadLocal基本使用1.2.1、常用方法1.2.2 使用案例 1.3 ThreadLocal类与synchronized关键字 2、运用场景_事务案例3、ThreadLocal的内部结构4、 ThreadLocal的核心方法源码5、ThreadLocalMap源码分析5.2 弱引用和内存泄漏 课程地址&#xff1a; ht…

【大学英语视听说上】“智力”口语问答练习

题目&#xff1a; book 2, page 9, question 4 回答&#xff1a; 1: What do you think of the view “Intelligence must be bred, not trained”? I think this view is biased. The view suggests that intelligence is primarily determined by genetic factors and inh…

drawio画图工具的四种使用方式

1、免安装使用&#xff08;绿色版&#xff09; 这种直接下载下来直接就可以使用&#xff0c;属于绿色版&#xff08;开箱即用&#xff09;&#xff0c;适用于个人 点击下载地址 2、 安装使用 这种下载下来就需要安装才可使用&#xff0c;适用于个人 点击下载地址 3、war包…

P3 Linux应用编程:系统调用与库函数

前言 &#x1f3ac; 个人主页&#xff1a;ChenPi &#x1f43b;推荐专栏1: 《C_ChenPi的博客-CSDN博客》✨✨✨ &#x1f525; 推荐专栏2: 《Linux C应用编程&#xff08;概念类&#xff09;_ChenPi的博客-CSDN博客》✨✨✨ &#x1f6f8;推荐专栏3: ​​​​​​《 链表_Chen…

轻量封装WebGPU渲染系统示例<42>- vsm阴影实现过程(源码)

前向实时渲染vsm阴影实现的主要步骤: 1. 编码深度数据&#xff0c;存到一个rtt中。 2. 纵向和横向执行遮挡信息blur filter sampling, 存到对应的rtt中。 3. 将上一步的结果(rtt)应用到可接收阴影的材质中。 具体代码情况文章最后附上的实现源码。 当前示例源码github地址: …

Azure Machine Learning - 使用 Python 进行语义排名

在 Azure AI 搜索中&#xff0c;语义排名是查询端功能&#xff0c;它使用 Microsoft AI 对搜索结果重新评分&#xff0c;将具有更多语义相关性的结果移动到列表顶部。 关注TechLead&#xff0c;分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验&am…

基于单片机的多功能视力保护器(论文+源码)

1.系统设计 多功能视力保护器在设计过程中能够对用户阅读过程中的各项数据信息进行控制&#xff0c;整体设计分为亮种模式&#xff0c;分别是自动模式&#xff0c;手动模式。在自动模式的控制下&#xff0c;当单片机检测当前光照不强且有人时就开启LED灯&#xff0c;并且会根据…

实现跨平台高手必修的课程,玩转Flutter动态化的解决的一些精华部分总结

Flutter作为一种快速、可靠的跨平台移动应用开发框架&#xff0c;在动态化方面也有很多令人兴奋的特性。本文将总结Flutter动态化的一些精华部分&#xff0c;帮助开发者更好地利用这些功能。 正文&#xff1a; 在实现跨平台高手必修的课程中&#xff0c;Flutter动态化是一个不…

kubernetes七层负载Ingress搭建(K8S1.23.5)

首先附上K8S版本及Ingress版本对照 Ingress介绍 NotePort&#xff1a;该方式的缺点是会占用很多集群机器的端口&#xff0c;当集群服务变多时&#xff0c;这个缺点就愈发的明显(srevice变多&#xff0c;需要的端口就需要多) LoadBalancer&#xff1a;该方式的缺点是每个servi…

人工智能学习5(特征抽取)

编译环境&#xff1a;PyCharm 文章目录 编译环境&#xff1a;PyCharm 特征抽取无监督特征抽取(之PCA)代码实现鸢尾花数据集无监督特征抽取 有监督特征抽取(之LDA)代码实现,生成自己的数据集并进行有监督特征抽取(LDA)生成自己的数据集PCA降维和LDA降维对比 代码实现LDA降维对鸢…

mysql服务日志打印,时区不对的问题

查资料发现 原来日志的时区和服务器的时区不是一个参数控制的 log_timestamps 单独控制日志的时区 show global variables like log_timestamps;看到默认的是UTC&#xff0c;只需要修改为和系统一致就行 #数据库中直接修改 set global log_timestampsSYSTEM;#配置文件my.cn…

NacosSync 用户手册

手册目标 理解 NacosSync 组件启动 NacosSync 服务通过一个简单的例子&#xff0c;演示如何将注册到 Zookeeper 的 Dubbo 客户端迁移到 Nacos。 介绍 NacosSync是一个支持多种注册中心的同步组件,基于Spring boot开发框架,数据层采用Spring Data JPA,遵循了标准的JPA访问规范…

面试题:MySQL为什么选择B+树作为索引结构

文章目录 前言二、平衡二叉树(AVL)&#xff1a;旋转耗时三、红黑树&#xff1a;树太高四、B树&#xff1a;为磁盘而生五、B树六、感受B树的威力七、总结 前言 在MySQL中&#xff0c;无论是Innodb还是MyIsam&#xff0c;都使用了B树作索引结构(这里不考虑hash等其他索引)。本文…

视频生成的发展史及其原理解析:从Gen2、Emu Video到PixelDance、SVD、Pika 1.0

前言 考虑到文生视频开始爆发&#xff0c;比如11月份就是文生视频最火爆的一个月 11月3日&#xff0c;Runway的Gen-2发布里程碑式更新&#xff0c;支持4K超逼真的清晰度作品(runway是Stable Diffusion最早版本的开发商&#xff0c;Stability AI则开发的SD后续版本)11月16日&a…

wordpress安装之Linux解压缩安装

本次教程是为了让大家少走弯路&#xff0c;可以更直观的去认识我们不懂的知识面。 首先我们安装解压缩的软件 命令如下&#xff1a; yum install -y unzip 上一篇我们讲到传输文件了 这篇我们把传输过来的压缩包解压并进行安装。follow me&#xff01; 我们输入命令 unzi…

Spring Cloud NetFlix

文章目录 Spring Cloud1 介绍2 Eureka&#xff08;服务注册与发现&#xff09;2.1 介绍2.2 服务注册与发现示例2.2.1 Eureka Server&#xff1a;springcloud-eureka2.2.2 Eureka Client&#xff1a;springcloud-provider2.2.3 Eureka Client&#xff1a;springcloud-consumer 2…

前端可滚动分类商品List列表 可用于电商分类列表

前端可滚动分类商品List列表 可用于电商分类列表 引言 在电商应用中&#xff0c;一个高效、用户友好的分类列表是提高用户体验和转化率的关键。本文将探讨如何使用xg-list-item、uni-grid和xg-tab等组件创建一个可滚动的分类商品列表&#xff0c;并处理相关的用户交互事件&…

Python容器——字典

Key——Value 键值对

canvas 轮廓路径提取效果

前言 微信公众号&#xff1a;前端不只是切图 轮廓 对内容做border效果&#xff0c;可以先看下代码运行的效果 内容是黑线构成的五角星&#xff0c;其轮廓就是红线的部分&#xff0c;本文主要介绍如何在canvas中实现这种效果 Marching Square 这里运用到的是marching square算法…