多线程编程带来的不安全问题

news2024/11/15 23:37:29

目录

1.观察线程不安全问题

2.出现线程不安全问题原因

2.1 根本原因

2.2 代码结构

2.3 原子性

2.4 内存可见性问题

2.5指令重排序

3.通过原子性解决线程安全问题

4.synchronized的使用方法

4.1 修饰方法

4.2 修饰代码块


1.观察线程不安全问题

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的

如果没有多线程,代码的执行顺序就是固定的,代码执行顺序固定,那么程序的结果也就是固定的,有了多线程,在抢占式执行,随即调度的机制下,代码的执行顺序就会发生改变,产生更多的变数!

代码执行顺序的可能性从一种情况变成无数种执行顺序,所以要保证无数种线程调度顺序的情况下,代码执行结果是正确的,如果不正确,线程就是不安全的!!

下面通过代码感受线程安全问题

我们定义两个线程,分别对count进行自增50000次,我们的预期结果是count为100000,在线程安全的前提下是这样的,如果线程不安全,结果肯定有差异,那如何让线程安全呢?

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

}
public class ThreadDemo14 {

    public static void main(String[] args) {
        Counter counter = new Counter();
        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();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("counter = "+counter.count);
    }
}

多次运行后的结果

 我们预期的结果是十万,这里的结果不是并且每次结果都不一样,因此这是一个多线程带来的bug!!!

我们分析一下原因

自增一次操作分为三步,先把内存中的值读取到cpu的寄存器中(load),把cpu中的数值+1运算(add),最后把结果写到内存中(save)

由于线程是抢占式执行,两个线程中的这三个步骤中执行到任意一个指令时,线程都可能被被调度走,cpu让别的线程来执行

其中,这两个情况是没有问题的,线程安全

第一种情况

 t1线程执行三个步骤,对count++后count为1,然后t2对count操作,count++后写入内存中,自增两次,count结果为2,正确.另一种情况和这种情况相反,t2先让count ++后写入内存,此时t1加载出的count是1,自增后count为2,写入内存

来看这种线程不安全的情况

 t1先加载,并且自增,count等于1,但是此时没有写入内存,t2线程就开始加载,count为0,自增后为1,然后写入内存,count为1,然后t1线程写入内存,count还是1!!!就在这里出现了问题

这种情况也不安全,t1先load,此时count=0,然后t2load, 此时count=0.然后自增,count=1,写入内存中,然后t1自增,count=1,写入内存中,后一次的自增覆盖了前一次的自增,count还是1!经历了两次自增,count结果还是1,这就出bug了!

类似于之前提到事务"读未提交 read uncommitted"是相同的,相当于t1读到的是t2还没来得及提交的数据,并发事务和多线程都属于并发编程问题

这就是count结果不是十万的原因,但是也有可能结果恰好是正确的,如果线程每次调度的顺序都是上面提到的正确的顺序,结果就是正确的,但是可能性很小!

结果不是10W,那么结果一定都会大于5w吗,也不一定,如果全都出现了两个线程都自增一次,count+1这种情况,结果就不大于5w,或者,t1自增1次,t2自增了2次,count最终还是+1这种情况,就小于5w

 这就属于,t2中的count自增无论多少次,还是会被t1最后给覆盖了,(t1load时count为0)count还是1

2.出现线程不安全问题原因

2.1 根本原因

抢占式执行,随即调度

2.2 代码结构

多个线程同时修改同一个变量(一个线程修改一个变量,安全.多个线程读取同一个变量,安全,像String对象,不可变对象,天然石线程安全的,无法修改,只能读取.多个线程修改多个不同的变量,安全)

因此可以通过调整代码结构来规避这个问题,这种调整不一定都能使用,不是一个普适性的方案

2.3 原子性

如果修改操作是原子的,那就线程安全,但是像上述案例中,非原子的,那就有很大概率出现线程安全问题!

那么如何让操作变成原子的呢?引入了重要的概念----加锁

2.4 内存可见性问题

如果一个线程读,一个线程改,那么读到的结果可能不符合预期

2.5指令重排序

本质是编译器的优化出问题了 ,把代码调整了,保持逻辑不变的情况下,调整代码的执行顺序,可能会出问题

这几个问题是典型的线程不安全问题的原因,但是线程是否安全远不止这些原因,多线程运行代码 ,不出bug就是安全的!!

3.通过原子性解决线程安全问题

通过"加锁操作"把非原子的操作转化为原子的操作

我们对第一个案例进行改动

 对方法加了synchronized之后,进入该方法,就会加锁,出了方法就会解锁

一个线程获取到🔒之后,除非他主动释放,否则不能强占

如果两个线程同时获取这个锁,只有一个线程能成功获取到,另一个没获取到的线程阻塞等待(BLOCKED)一直等待到上一个线程释放锁之后,当前线程才能获取到🔒

lock的阻塞就把刚才的t2的load推迟到t1的save之后了,也就避免了脏读问题!!(在t1执行提交数据后,t2再读数据)

再来看效果,多次执行后都是准确的结果,线程安全问题得到解决!!

 加锁之后,代码的执行速度肯定会降低,但是为了主线任务,保证结果的正确性,是必须要加锁的

4.synchronized的使用方法

加锁要明确对哪个对象进行加锁,如果两个线程同时对一个对象加锁,就会产生阻塞等待(锁竞争/锁冲突),如果两个线程对不同对象加锁,不会产生锁竞争/冲突

4.1 修饰方法

修饰普通方法,进入方法加锁,出了方法解锁

锁的对象是this,哪个对象调用了方法,this就指向这个对象,即对这个对象加锁

这种情况就是把synchronized加到方法上了,相当于针对this加锁,当t1线程调用add()后,counter就加上锁了,另一个线程执行add()的时候,也尝试对counter 加锁,但是此时t1已经加过锁了,此时t2的加锁操作就会阻塞等待

修饰静态方法

锁的对象是类对象,和修饰普通方法同理

4.2 修饰代码块

需要显式指定锁对象,也就是手动指定🔒加到哪个对象上

进入代码块就加锁,出代码块解锁

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

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

相关文章

餐饮企业提升用户价值,从基于点单链路的精细化运营开始

近几年&#xff0c;餐饮业的经营增长面临着巨大挑战&#xff0c;在这种情况下&#xff0c;餐饮企业如何提升用户价值&#xff0c;提高多风险下持续增长的动力呢&#xff1f;神策数据杨丽月聚焦用户运营&#xff0c;围绕“一条链路&#xff0c;四个指标”&#xff0c;跟大家分享…

C# 标签式导航

一 标签式导航 使用TabControl控件&#xff0c;将多个页面结合起来&#xff0c;每个标签关联一个页面。 TabControl 选项卡容器&#xff1b; TabPage选项页&#xff1b; 可以在设计器里添加页面&#xff0c;也可以代码方式添加。 操作演示&#xff1a; ① 选中TabControl; ②…

框架skywalking部署笔记

一旦你的程序docker化之后,你会遇到各种问题,比如原来采用的本地记日志的方式就不再方便了,虽然你可以挂载到宿主机,但你使用 --scale 的话,会导致 记录日志异常,所以最好的方式还是要做日志中心化,另一个问题,原来一个请求在一个进程中的痉挛失败,你可以在日志中巡查…

linux的内存映射(二)

我们先来看幅图&#xff1a; Linux内存管理的最底层是buddy内存管理方案&#xff0c;即伙伴算法&#xff0c;管理伙伴算法我们不做详诉&#xff0c;有兴趣的可以自行学习&#xff0c;我们这里只要知道buddy内存池中只能分配2^n个page的内存&#xff0c;比如1,2,4,8……个pages…

前端面试指南之JS面试题总结

1. JS 有哪些数据类型&#xff1f; 根据 JavaScript 中的变量类型传递方式&#xff0c;分为基本数据类型和引用数据类型两大类七种。 基本数据类型包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增)六种。 引用数据类型只有Object一种&#xff0c;主要包括对象…

pve扩展lvm空间,重做物理卷

pve扩展lvm空间&#xff0c;重做物理卷pve扩展lvm空间&#xff0c;重做物理卷必备知识扩展步骤1.查看当前机器状况2.查看盘符3.确定是否有可分配空间4.确定物理卷分配情况5.&#xff08;可略&#xff09;如果你的空闲的盘已经挂载了请用以下命令卸掉6.创建物理卷7.查看卷状态8.…

day7_redis学习

文章目录Geo查找附近商铺BitMap实现用户签到UV统计Geo查找附近商铺 Geo是GeoLocation的简称&#xff0c;代表地理坐标&#xff0c;在Redis 3.2中加入了对Geo的支持&#xff0c;允许存储地理坐标信息&#xff0c;常见的命令有: ①GEOADD key x y member [x2 y2 member]&#xf…

(一)SpringBoot项目初始化并引入基础文件【木字楠后台管理系统模板】

&#x1f3b6; 文章简介&#xff1a;SpringBoot项目初始化并引入基础文件【木字楠后台管理系统模板】 &#x1f4a1; 创作目的&#xff1a;为了带大家完整的体验木字楠后台管理系统模版的开发流程 ☀️ 今日天气&#xff1a;天气☁️很好。太阳晒在身上暖暖的 &#x1f4dd; 每…

IB数学怎么学,HL是什么难易程度,和SL的内容差的大吗?

做为刚大学毕业的M20 IBer&#xff0c;数学课以往一直没下过90%&#xff0c;全是7分&#xff0c;IA 校园内得分19/20。尽管final被IBO赏了个6&#xff08;大考撤销&#xff0c;ibo蒙蔽估分orz&#xff09;&#xff0c;但我还是汇总了一些自身的数学学习工作经验&#xff0c;期待…

一个变量命名神器:支持中文转变量名

变量命名的规范&#xff0c;对于我们编程&#xff0c;大家都知道是非常重要的&#xff0c;上次给大家推荐过一个命名辅助工具《程序员还在为变量取名苦恼&#xff0c;那是因为你不知道&#xff0c;这个变量命名神器》&#xff0c;但大家一致反馈存在2个问题&#xff1a; 1、网…

项目管理(如何进行项目采购管理)

项目采购管理包括从项目团队外部采购或获取所需产品、服务或成果的各个过程。项目采购管 理包括编制和管理协议所需的管理和控制过程,例如,合同、订购单、协议备忘录 (MOA),或服务 水平协议 (SLA)。被授权采购项目所需货物和(或)服务的人员可以是项目团队、管理层或组织采…

使用winhex对fat16文件系统分析

“FAT16”是“File Allocation Table,16-bit”的英文缩写,意思是“文件分配表,16位” 。FAT16文件系统是从微软的DOS 3.0系统开始使用的&#xff0c;它能够支持大于16MB小于2GB的分区&#xff0c;Windows 2000以上操作系统可以创建4GB的FAT16分区&#xff0c;但与传统的FAT16不…

【观察】深度剖析,为什么说帆软的文化决定了FineBI6.0的易用和好用

毫无疑问&#xff0c;今天国家对数字经济给予了前所未有的高度重视&#xff0c;《“十四五”数字经济发展规划》中&#xff0c;就明确将继续坚持推进数字产业化和产业数字化&#xff0c;赋能传统产业转型升级&#xff0c;为构建数字中国提供有力支撑&#xff0c;并提出到2025年…

FFmpeg之硬解码

导读 众所周知&#xff0c;软解码虽然兼容性一流&#xff0c;但是却非常依赖CPU&#xff0c;所以性能消耗笔记大&#xff1b;硬解码使用内置的DSP芯片进行解码&#xff0c;性能高&#xff0c;但是兼容性一般。 虽说硬解码兼容性不太好&#xff0c;但是在实际开发中出于对性能的…

智能化煤矿-设备管理系统、故障诊断、全生命周期管理

随着智能化煤矿的建设&#xff0c;煤矿设备、传感器数量在增加、煤矿设备的自动化、智能化程度也相对提高。保证设备稳定运行&#xff0c;减少故障时间是提高煤矿安全生产的一种重要途径。另外随着信息技术的发展&#xff0c;像云计算、物联网、大数据等相关技术的运用&#xf…

vue2的 webpack-bundle-analyzer 打包体积 看优化包

1、 先安装 npm i webpack-bundle-analyzer -D 2、 vue.config 文件中 配置 /* webpack相关配置 *该对象将会被 webpack-merge 合并入最终的 webpack 配置 */ if (process.env.use_analyzer) { // 分析 config .plugin(webpack-bundle-analyzer) .use(require(webpa…

XuperSocial首个明星DApp上线,探索区块链分布式通信基础设施

12月7日&#xff0c;星际口袋上线星际社区&#xff0c;为广大藏友提供稳定、可信的交流场所。星际社区基于百度超级链团队重磅发布的去平台化社交解决方案XuperSocial搭建&#xff0c;XuperSocial是架设在百度超级链开放网络&#xff08;XuperOS&#xff09;上的DApp&#xff0…

Oracle PrimaveraUnifier 之数据要素(Data Element)

目录 一&#xff1a;什么是数据要素 二&#xff1a;常用的数据要素 一&#xff1a;什么是数据要素 数据要素是Oracle Primavera Unifier维护业务单据/表达的最小单元&#xff0c;也就是我们常说的字段&#xff0c;它将数据定义与字段标签相结合&#xff0c;成为我们用户在 P…

Runtime源码解析-类中bits

Runtime源码解析-类中bits class_rw_t ro_or_rw_ext_t 成员变量方法 初始化方法存取方法类型判断 公有方法 获取class_rw_ext_t获取/设置class_ro_t方法、属性、协议列表 class_rw_ext_tclass_ro_t总结 1. 为什么ro_or_rw_ext 会有两种类型&#xff0c;class_rw_ext_t或者cla…

Sping Boot 如何实现自动配置

Sping Boot 如何实现自动配置 Spring Boot都需要创建一个mian启动类&#xff0c;而启动类都含有SpringBootApplication注解&#xff0c;从启动类&#xff0c;一步步探索源码。 SpringBootApplication注解 Spring Boot 启动类上都有一个 SpringBootApplication注解&#xff1…