Java中的线程安全问题(多线程重点)

Java基础【重点】多线程————线程安全问题 java 多线程 线程安全问题 阅读详情


引起线程安全问题的原因大概有以下五种

1.各个线程在系统中抢占式执行(根本原因)

我们之前提到过,操作系统在执行各个线程的时候是具有随机性的,我们并不能确定谁先被执行谁后被执行,这样一来就很可能引发很多bug,这是引发线程安全问题的根本原因,也是我们无可奈何的原因,因为我们不可能改变操作系统。

2.多个线程对同一个变量执行修改操作

我们这里指的是多个线程对于同一个变量/对象进行修改,如果多个线程对多个变量进行修改,那么并不一定会引发安全问题,或者多个线程对同一个变量只读不改,那么也同理

3.针对的变量/对象操作不是原子的

原子性,我们在学习数据库事务这一块曾接触过,操作的原子性大概就是指,要不就不执行,要执行就一口气的执行完,因为在修改变量时可能涉及到多个CPU的操作指令,这些指令并不被包装成一个原子性的指令,因此很可能引发一些bug

针对2,3的一个例子

我们在这里设想一个情况,如果有两个线程,同时对同一个变量实现自增操作,比方说每个线程自增1w次,那么会发生什么样的结果呢,根据推演应该是该变量从0变成了20000

    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

在这里插入图片描述
但是经过我们多次执行后,发现该变量的最终结果始终在10000-20000之间
这是为什么呢,这其实就是我们上面所提到的线程安全问题的2和3,我们向底层看去,线程在实现count++操作时,实际上是要有三个步骤
第一步:load (将变量的值从内存中加载到CPU寄存器中)
第二步:add (CPU将该寄存器中的值++)
第三步:save(将CPU寄存器中的值写入内存中)
t1和t2线程每一次对count++的时候,都要执行这三个指令,但是由于线程的抢占式执行,导致了这三个指令在两个线程中并发执行的顺序多种多样,十分不确定
在这里插入图片描述
如果箭头顺序是执行指令的顺序,那么可能是两个线程串行执行(无论t1线程先还是t2线程先执行,结果都是同理的),那么这个时候,我们可以想象得到,t1先加载count(load),假设这时count是初识状态0,那么加载到寄存器中的count值为0,之后执行add操作,count变为1,最后save写回内存中,此时,内存中的count值为1,那么t2执行load操作时读取到寄存器中的值就是1,执行add后为2,save入内存,如此看来,这样的串行执行的顺序其实是没有问题的,那么问题其实出现在其他顺序中
在这里插入图片描述
当线程执行指令的顺序如上图时,我们再想一下,t1执行load,这时t1线程所对应的寄存器加载值为0,t2执行load,t2线程所对应寄存器count值也为0,之后t1执行add,t1寄存器count值为1,t1执行save,内存中count值被修改为1,之后t2执行add时,因为t2是t1add,save之前读取的数据,因此t2读取到的值是0,那么add也是将0变为1,之后t2的save操作,实际上是又将1写入到了内存中,相当于覆盖了t1save的结果,那么内存中最终的count值仍然是1,这样一来就出现了问题,因为抢占式执行的随机性,并且修改这个操作也非原子操作,可以打乱其顺序,如此造成了bug,使得本应该为20000的结果为10000-20000之间(假设全部为串行执行,那么结果为20000,假设全部为混乱执行,那么结果为10000,因此结果一定在10000-20000之间)

解决办法:加锁(synchronized)修饰变量/对象

class Count{
    public int count = 0;

    synchronized public void add(){
        count++;
    }
}
public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count.add();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                count.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.count);
    }
}

在这里插入图片描述
可以看到我们在count类的add方法加了锁,这样的原理是,加了锁的方法不可以重复加锁,相当于通过锁,将count++这个操作中的三个步骤(三个指令)包装成了原子性的,当一个线程加锁执行方法时,另一个线程进入阻塞态,因为已经有锁,必须等该锁解开,才可以再加锁,这样强行的使得操作原子化,使得两个线程一定串行执行,所以最终的结果:
在这里插入图片描述
因此并发性越强,越容易出bug,但是并发性越弱,串行性越强,程序执行时间就越长,这是没有办法的

4.内存可见性所引发的问题(编译器优化导致)

我们很多语言的编译器现在都很智能,会在程序员编写代码后,在不影响原来逻辑的情况下进行一系列优化,这样的优化,在针对单线程时,基本上是不会出问题的,但是我们的多线程的执行顺序不确定,导致了这一部分优化可能会改变了逻辑顺序,甚至产生bug,内存可见性所引发的问题就是由于编译器优化所产生的

例子:

我们来看一个例子,如果创建两个线程,一个线程不断的循环执行读操作,另一个线程执行对读操作线程的循环条件的修改,类似于中断线程的一个操作,我们看一下会发生什么:

    static boolean flg = true;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(flg){

            }
            System.out.println("读操作线程结束!");
        });
        t1.start();
        Scanner scanner = new Scanner(System.in);
        flg = scanner.nextBoolean();
        System.out.println("main线程结束!");
    }

在这段代码中,我们自定义了一个标志位flg,在main线程中修改了它的值,如果读线程结束,则会输出“读操作线程结束!”
在这里插入图片描述
然而,我们发现当我们输入了false之后,我们发现main线程都结束了也并没有输出读操作线程结束,这样一来就产生了bug,产生这样bug的原因,其实是因为读操作线程中,不断地循环空语句,这样在while的判断中,读线程不断快速的访问内存中的flg的值,实际上是一个很抵消的操作,因为访问内存是一个很浪费时间的操作,这时,编译器就大胆的认为flg的值不变,所以会直接访问读线程所对应的寄存器中的flg的值,也就是执行了一次load操作之后,读操作寄存器中的值是true,之后一直直接访问寄存器中的true,以此来优化代码,但是main线程中执行修改,这一切所对应的flg的改变关系只存在于内存和main线程所对应的寄存器之间,而读线程所对应的寄存器始终没有改变,即使内存中的flg值已经改变了,读线程寄存器中的flg值仍然是之前读到的true,因此读线程没有结束。

解决方法:加锁(synchronized)/volatile

我们在遇到这种编译器优化所带来的问题时,可以采用加锁,加锁可以保证所有的问题的解决,也可以使用volatile,volatile是手动的取消了编译器的优化,但是并没有将操作原子化
在这里插入图片描述
当我们将flg用volatile修饰之后,可以发现

在这里插入图片描述
main线程和读操作线程都结束了,这说明我们的修改被读线程从内存中读取到了,这样内存可读性问题得以解决

5.指令重排序问题(编译器优化导致)

这个问题比较抽象,我们拿一个图来举例
在这里插入图片描述
如果妈妈让我们买菜,写的顺序是上图的茄子,鸡蛋,黄瓜,番茄,那么我们的路线就是
在这里插入图片描述
很明显这并不是一个最优最短线路,反而浪费了很长时间,那么这个时候编译器就会自动优化顺序为1.鸡蛋 2.黄瓜 3.茄子 4.番茄,这样一来线路就是

在这里插入图片描述
很明显,大大缩短了路程,但是执行这些指令的顺序发生了改变,因此也很有可能出现问题

总结原因

1.线程调度优先级不一定(抢占式执行) -----根本原因
2.多个线程对同一个变量进行修改操作(多个线程对多个变量操作不算在内,读操作也不算在内)
3.针对的变量操作不是原子的(要不不执行,要不就一口气执行完毕)
(比方说读操作,只需要一条机器指令,属于原子的操作,通过加锁,可以将不原子的操作打包成原子的)
4.内存可见性所带来的问题(编译器优化)
5.5.指令重排序(编译器优化)

Python进行.tif格式数据波段组合;多波段tif数据波段分离 Python进行.tif格式数据波段组合;多波段tif数据波段分离.tif格式数据波段组合多波段.tif数据波段分离 阔别已久,我又来了;啊啊啊…… 读了研究生,忙到天昏地暗(哈哈哈,这都是借口,主要是花了一定时间适应研究生生活) 这方面的内容我也主要是应用在遥感领域,我们知道在遥感应用中,大多数的图像数据格式为.tif格式的。进行波段组合使用ArcGIS里面的Composite Bands工具是可以实现的,波段分离也可以使用一些工具或巧妙的方法分开。但当你使用Python程序的话,就可以简化这个软件点击和 阅读详情

相关推荐

ESP32-C5 Wi-Fi CSI感知技术原理与工程实践

Wi-Fi信道状态信息(CSI)是OFDM系统中反映多径信道特性的复数频域响应,源于导频子载波的信道估计过程,本质属于物理层原始观测数据。其高维度(如56子载波)、高时间分辨率(百赫兹级采样)及零硬件增益的特性,使其成为无感人体感知的理想信号源。相比UWB或毫米波雷达,CSI方案依托现有Wi-Fi基础设施,兼具成本优势与部署灵活性,广泛应用于呼吸监测、跌倒检测、手势识别等边缘智能场景。乐鑫ESP32-C5通过5.8GHz低噪接收、亚纳秒级多天线同步与固件级CSI增强,显著提升微动感知鲁棒性,结合AGC锁定、

weixin_42153793的博客 343

【java篇】线程安全问题(大总结)

复习自学线程之线程安全问题大总结;

m0_64231944的博客 3692

GD32F4系列替换STM32F4,HAL库CAN初始化卡死?一个Sleep模式的坑与填坑实录

本文详细解析了GD32F4系列替换STM32F4时,HAL库CAN初始化卡死的问题根源——Sleep模式差异。通过对比两款芯片CAN控制器的默认状态,提出了不修改HAL库源码的解决方案,即在HAL_CAN_MspInit()中清除SLEEP位,并分析了该方法的优势。文章还提供了国产MCU替换的通用建议和调试技巧,帮助开发者高效解决问题。

weixin_29174013的博客 217

Java多线程之线程安全问题

Java多线程中的线程安全问题, 解决方法, 线程安全和不安全的标准类.

trong_ 的烂笔头 7225

基于价值的深度强化学习:DQN

本文主要是介绍DQN原理和算法伪代码和代码实现,算法实现包括pytorch版本和stable-baselines3版本

溯源的专栏 1083

别再死磕U-Net了!用PyTorch手把手教你搭建ResUnet,搞定医学图像分割(附完整代码)

本文详细介绍了如何利用PyTorch搭建ResUnet模型,通过引入残差连接解决医学图像分割中的梯度消失、特征退化和细节丢失问题。与传统U-Net相比,ResUnet在Dice系数和收敛速度上均有显著提升,特别适合处理脑部MRI等复杂医学图像。文章还提供了完整的代码实现和关键训练技巧,帮助开发者快速掌握这一先进技术。

weixin_42552637的博客 327

Java线程安全问题的原因和解决方案

线程安全的确切定义是比较复杂的,不过我们可以这样认为:当多线程环境下的代码运行的结果是符合我们预期的,即在单线程环境下应该得到的结果,则说这个程序是线程安全的,反之,则是线程不安全.注意:判定一个代码是否线程安全,要具体问题具体分析,不是加了锁就一定安全~

Original0的博客 997

EastWave应用案例:机箱屏蔽效能仿真

如何使用EastWave对机箱的电磁屏蔽效能进行仿真

2201_76034687的博客 1394

Java 线程安全

Java 线程安全问题剖析及如何控制多线程环境下,并发编程的执行顺序。

weixin_63025863的博客 2020

微机原理课程设计-模拟十字路口交通信号灯

一、设计任务描述 1.1设计目的 通过课程设计使学生更进一步掌握微机原理及应用课程的有关知识,提高应用微机解决问题的能力,加深对微机应用的理解。通过查阅资料,结合所学知识进行软、硬件的设计,使学生初步掌握应用微机解决问题的步骤及方法。为以后学生结合专业从事微机应用设计奠定基础。 1.2 设计要求 控制LED灯,按照十字路口红绿灯的原理亮灭显示: (1) 东西方向车辆放行x秒...

Learning_is_endless的博客 2万+

SwarmClaw:自托管多智能体AI运行时部署与实战指南

多智能体系统是人工智能领域的重要发展方向,它通过多个具备自主决策和协作能力的智能体共同完成任务,其核心原理在于分布式问题求解与协同规划。这种架构在提升任务处理效率、复杂问题分解和鲁棒性方面具有显著技术价值,广泛应用于自动化工作流、虚拟团队协作、智能客服和个性化助手等场景。本文聚焦于SwarmClaw这一自托管的多智能体AI运行时,它原生支持Model Context Protocol,并内置了超过23种AI提供商,为开发者提供了一个开箱即用且高度可定制的控制平面,便于构建和管理复杂的多智能体协作网络。

weixin_27207591的博客 401

Java EE:2.多线程-初阶(第三弹):线程的状态+线程安全问题[重点]

Java EE:2.多线程-初阶(第三弹)

要努力去发光,而不是被照亮~ 547

低功耗边缘设备部署指南:YOLO26 INT8量化与TensorRT加速全流程

将YOLO26 INT8部署到低功耗边缘设备,核心原则是“校准为王、敏感层精准回退、端到端验证”。校准集≥500张,来自真实推理场景,未经训练增强校准集预处理与推理pipeline逐行对齐trtexec命令必带--fp16作为fallback量化后用polygraphy定位敏感层,而非盲猜仅回退必要层(通常2-3个),保留INT8收益在真实部署场景测试集上验证mAP,不以COCO为准C++部署使用异步推理+双缓冲nsys profiling确认无传输瓶颈。

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 212

【JavaEE初阶】线程安全问题及解决方法

线程安全的概念 想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为: 如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。 线程安全,在单线程环境下和多线程环境下都不会出现问题。

m0_61876562的博客 1642

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

什么是线程安全问题?如何确保线程安全?进来看看就明白了!!

🌈🌈🌈今天给大家分享的是:什么是线程安全,在程序中多线程并发执行的时候,是否会产生线程不安全问题,以及如何解决线程不安全问题。🛩️🛩️🛩️希望我的文章能对你有所帮助,有不足的地方还请各位看官多多指教,大家一起学习交流!🛩️🛩️🛩️在此谢过啦!哈哈哈!✈️✈️✈️。

m0_73920844的博客 3844

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

多线程初阶(四):线程安全问题&锁&synchronized

线程安全问题---锁的引入(synchronized关键字+代码块)

2401_83595513的博客 1822

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

Java 单例模式及线程安全问题

单例的目的是为了保证运行时Singleton类只有唯一的一个实例,用于一些较大开销的操作。 实现单例模式的方式 饿汉式 public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } } 由于

LittleSkey的博客 2473

Java 多线程(四)—— 线程安全 与 volatile 与 单例模式

介绍Java 的线程安全,volatile 关键字,以及单例模式的原理与实现

熵减玩家 4467

Java面试——多线程面试题

0.前言在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分,本文汇总了常见的一些多线程面试题。一些问题,比如volatile关键词的作用,synchronized和ReentrantLock的区别,wait()和sleep()的区别等等问题,已经在之前写过的文章中提到过了,这里就不赘述了,有兴趣可以查看以下几篇文章:Java并发——线程同步volatile与synchronized详

关于Android开发的一些技术点总结 ╮( ̄▽ ̄”)╭ 2万+

Java中的线程安全问题与解决方案

1.背景介绍 1. 背景介绍 线程安全是指多个线程并发访问共享资源时,不会导致资源的不正确或不一致的状态。在Java中,线程安全问题通常发生在多线程环境下,当多个线程同时访问和修改共享资源时,可能导致数据不一致或其他不正确的状态。 线程安全问题在Java中非常常见,可能导致严重的后果,例如数据库连接泄漏、资源耗尽、程序崩溃等。因此,了解线程安全问题的原因和解决方案非常重要。 本文将从以下几...

AI天才研究院 870

多线程的各种状态与线程安全问题及wait和notify详解

1.NEW:安排了工作,还未开始行动Thread 对象创建好了.但是还没有能用start方法});t.start();2.TERMINATED:工作完成了.try {});//t还没运行t.start();t.join();//t一定结束了3.RUNNABLE:可工作的.又可以分成正在工作中和即将开始工作.可理解成两种情况:1)线程正在cpu上运行;2)线程在这里排队,随时都可以去cpu上执行.try {});t.start();//t.join();

weixin_63018754的博客 997

【手撕JAVA多线程:2.线程安全】 2.1.JVM层面的线程安全保证

从设计角度看JVM层面给出的线程安全保证

BugMan的博客 1097

Java 多线程系列Ⅶ(线程安全集合类)

在数据结构中,我们学习过 Java 的内置集合,但是我们知道,我们学过的大多数集合类都是线程不安全的,少数如VectorStackHashTable是线程安全的,但这些都是一些比较“粗糙”的类(在所有方法上加了 synchronized 锁),一般不建议使用。那么当我们想要在多线程下使用集合类该怎么处理呢?

LEE180501的博客 632

内置锁的核心原理之线程安全问题

i++自增线程不安全吗?你所理解的自增要怎么样才能安全呢?

ITwolf的博客 4107

多线程访问static变量是否存在并发问题

类的成员分为两类,静态成员(static member)和实例成员(instance member)。静态成员属于类;实例成员则属于对象,即类的实例。 先看一个类: public class staticDemo { static int result; static int Addone(Integer num){ Integer inner_result=...

知之者不如好之者,好之者不如乐之者 1万+

【多线程】线程安全问题出现的原因以及解决方法

线程安全问题出现的原因以及解决方法

m0_74105656的博客 902
上一篇: Java中线程的状态
下一篇: 刷题精选:顺时针输出递增数组(螺旋递增升天数组)
bit_zhy
博客等级 码龄5年 370粉丝 15原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值