线程安全的原因及解决方法

什么是线程安全问题

线程安全问题指的是在多线程编程环境中,由于多个线程共享数据或资源,并且这些线程对共享数据或资源的访问和操作没有正确地同步,导致数据的不一致、脏读、不可重复读、幻读等问题。线程安全问题的出现,通常是因为线程之间的并发执行导致了数据竞争(Race Condition)或者时序问题(Timing Issues)。以上是网上找到的回答,我认为只要是在多线程代码实现产生bug都可以称为线程安全问题.

线程安全问题举例


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

这串代码预期结果是10000,但是无论执行多少次都达不到预期的效果

 这里主要原因涉及到指令的执行顺序

因为很多操作在cpu上都会又细分为多个操作,例如count++分为 load,add,save,多线程不能穿插执行,必须要等第一个线程操作完数据保存到内存后,第二个再执行,不正常的执行顺序,可能会将其中一个线程操作的数据覆盖等影响,大概率结果会有错误

线程安全的五大原因

1.系统调度是随机的:

线程在系统中随机调度,是抢占式执行的,这种情况我无法修改和干预,当多个线程访问并修改同一内存位置的数据时,由于线程的随机调度,可能导致数据的不一致性

2.原子性问题:

某些操作(如自增、自减等)在单线程环境下是原子的,但在多线程环境下可能不是原子的。这些操作可能被拆分成多个步骤,(希望的是每个cpu指令都是原子的,要么不执行,执行就执行完为止)从而导致数据的不一致性,上面的代码线程不安全主要是原子性问题

3.内存可见性问题:

由于Java内存模型的原因,一个线程对共享变量的修改可能无法立即被其他线程看到。这可能导致线程读取到旧的数据值,从而引发问题

4指令重排序:编译器和处理器为了提高性能,可能会对指令进行重排序。但在多线程环境下,这种重排序可能会破坏代码的语义,导致线程安全问题。

线程安全问题的解决方法

加锁synchronized

public class ThreadDemo1 {public static int count;public static void main(String[] args) throws InterruptedException {String s="锁无所谓是什么变量";Thread t=new Thread(()->{for (int i = 0; i < 5000; i++) {synchronized (s){count++;}}});Thread t1=new Thread(()->{for (int i = 0; i < 5000; i++) {synchronized (s){count++;}}});t.start();t1.start();t.join();t1.join();System.out.println(count);}
}

这样锁就把count打包成一个操作了,是原子性,当执行count的时候就不会出现执行一半的情况

加锁后需要注意几点

1.此时count++操作是串行执行,其余操作例如for循环都是并行执行

2.如果两个线程同时尝试加锁,此时只有一个线程可以获取锁成功,而另一个线程就会阻塞      等待。阻塞到另一个线程释放锁之后,当前线程才能获取锁成功。

3.当t1释放锁之后,t1线程还是会同时争夺这把锁

可重入锁

可重入锁指的是,本身加锁的线程能够加第二次锁,直接通过不会阻塞(写错了也能执行),下面的例子就是给一个程序加了两次锁,依旧可以执行成功

public class ThreadDemo1 {public static void main(String[] args) {Object locker =new Object();Thread t1=new Thread(()->{synchronized (locker){synchronized (locker){System.out.println("hi");}}});t1.start();}
}

死锁

死锁(Deadlock)是一个或多个线程因为竞争资源而造成的一种状态,在这种状态下,线程们会无限期地等待一个永远不会发生的条件,从而导致程序无法继续执行。

死锁的经典的三种场景

1.一个线程一把锁,如果是不可重入锁,在加上一把锁,就会出现死锁

2.两个线程,两把锁,第一个线程有A锁,第二个线程有B锁,第一个线程又尝试获取B锁,第二线程尝试获取A锁,就会出现死锁


public class ThreadDemo2 {public static void main(String[] args) {Object A=new Object();Object B=new Object();Thread t1=new Thread(()->{synchronized(A){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (B){System.out.println("t1两把锁");}}});Thread t2=new Thread(()->{synchronized (A){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (B){System.out.println("t2两把锁");}}});t1.start();t2.start();}
}

3.哲学家吃面问题/N个线程M把锁

        假设有五位哲学家围坐在一张圆形餐桌旁,每人面前有一碗面,每两个哲学家之间有一只筷子。因为用一只筷子很难吃到面,所以哲学家必须用两只才能吃到面,而他们只能使用自己左右手边的那两只。哲学家们有两种状态:进餐或思考。每当一个哲学家饥饿时,他会试图去拿他左右两边的筷子,但每次只能拿一只。只有当他拿到两只筷子时,才能开始进餐,吃完后需要放下餐叉继续思考。

在这个问题中,筷子是共享资源,而哲学家们对筷子的竞争可能导致死锁的发生。例如,如果每个哲学家都拿起左手边的筷子,等待右手边的餐叉变得可用,而右手边的筷子又被其他哲学家持有,那么就会形成死锁状态,因为每个哲学家都在等待其他哲学家释放资源,而这永远不会发生。

 怎么避免死锁
  1. 设置加锁顺序(最好的方法):线程按照一定的顺序加锁,确保每个线程在请求多个锁时都按照相同的顺序进行。这样可以防止循环等待的情况,从而降低死锁的风险。设置加锁顺序,使得每位哲学家在尝试拿起筷子时都遵循相同的顺序。例如,可以规定所有哲学家都先尝试拿起自己左侧的筷子,然后再拿起右侧的筷子。这样,在任何时候,最多只有一个哲学家能够拿起他左右两侧的筷子,从而避免了死锁的情况
  2. 避免使用多个锁:尽量将代码设计成只使用一个锁的情况,减少因为多个锁之间的依赖关系导致的死锁4。
  3. 设置加锁时限(超时重试):在获取锁的时候尝试加一个获取锁的时限,超过时限则放弃操作并释放之前获取到的锁,然后等待一段时间后进行重试。这种方法允许在没有获取锁的时候继续执行其他任务,减少死锁的可能性。

volatile

当一个程序读,一个程序写的时候,此时就容易出现内存可见性问题,volatile就是解决这个问题的,其中一个核心功能就是保证内存可见性

下面的例子创建了两个线程,一个线程不断判断变量的值死循环,另一个等待输入值,使死循环停止,当我们改变flag之后,t1线程并没有结束,这里解释一下原因是编译器发现每次循环都要读取内存,开销太大,于是就把读取内存操作优化为读取寄存器操作,提高效率,就导致写线程做出的修改,读线程感知不到

import java.util.Scanner;class Counter {public int flag;
}public class ThreadDemo {public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(()->{while(counter.flag==0){//此处为了代码简洁好演示,什么都不做}});Scanner sc = new Scanner(System.in);Thread t2 = new Thread(()->{counter.flag = sc.nextInt();});t1.start();t2.start();}
}

当我们在while循环中加上sleep后发现代码可以执行成功了,因为之前一直循环一秒钟可能循环上百亿次,在循环中加上sleep一秒钟也就执行上百次,大大减少开销

这里最主要的解决方法是加上volatile,告诉编译器这里不需要优化

public volatile int flag=0;//保证内存可见性,禁止指令重排序

 

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

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

相关文章

视频提取字幕怎么弄?5个快速获取视频字幕的方法

在忙碌而又充满活力的生活中&#xff0c;我们常常在通勤路上和午休间隙通过视频来获取信息和放松心情。 但有时候&#xff0c;我们想把视频里那些令人难忘的瞬间或关键信息保存下来&#xff0c;方便以后回顾或者分享。然而&#xff0c;手动摘录不仅费时&#xff0c;还容易漏掉…

【网络安全】实验七(ISA防火墙的规则设置)

一、实验目的 二、配置环境 打开两台虚拟机&#xff0c;并参照下图&#xff0c;搭建网络拓扑环境&#xff0c;要求两台虚拟机的IP地址要按照图中的标识进行设置&#xff0c;并根据搭建完成情况&#xff0c;勾选对应选项。注&#xff1a;此处的学号本人学号的最后两位数字&…

VRay渲染有什么技巧?渲染100邀请码1a12

渲染是视觉行业非常重要的一环&#xff0c;没有渲染就没有效果图&#xff0c;常用的渲染器有Vray&#xff0c;而Vray渲染有很多技巧&#xff0c;可以让渲染更快更省&#xff0c;下面我们总结下。 1、删除无用对象 检查场景&#xff0c;看是否有一些不需要渲染的物体和灯光&am…

时间处理的未来:Java 8全新日期与时间API完全解析

文章目录 一、改进背景二、本地日期时间三、时区日期时间四、格式化 一、改进背景 Java 8针对时间处理进行了全面的改进&#xff0c;重新设计了所有日期时间、日历及时区相关的 API。并把它们都统一放置在 java.time 包和子包下。 Java5的不足之处&#xff1a; 非线程安全&…

十一、作业

1.从大到小输出 写代码将三个整数数按从大到小输出。 void Swap(int* px, int* py) {int tmp *px;*px *py;*py tmp;} int main() {int a 0;int b 0;int c 0;scanf("%d %d %d", &a, &b, &c);int n 0;if (a<b){Swap(&a, &b);}if (a &l…

移动校园(2):express构建服务器,小程序调用接口,展示数据

express做服务器框架&#xff0c;mssql连接数据库&#xff0c;uni-request调用接口 这是文件夹目录 然后是index.js内容 const expressrequire(express) const appexpress() const uniRouterrequire("./uniRouter") const config{user:sa,password:123456,server:l…

基于YOLOv5的人脸目标检测

本文是在之前的基于yolov5的人脸关键点检测项目上扩展来的。因为人脸目标检测的效果将直接影响到人脸关键点检测的效果&#xff0c;因此本文主要讲解利用yolov5训练人脸目标检测(关键点检测可以看我人脸关键点检测文章) 基于yolov5的人脸关键点检测&#xff1a;人脸关键点检测…

批导会计凭证程序报错,通过监控点和消息类来定位触发的位置

ZFIU001 批导会计凭证报错&#xff0c;通过监控点和消息类来定位触发的位置 在使用程序导入会计凭证的时候&#xff0c;发现报错&#xff0c;后面找了很久很久的系统标准程序&#xff0c;打断点才找到这个位置&#xff0c;使用监控点还是可以比较快速找到报错的原因的&#xff…

QWidget窗口抗锯齿圆角的一个实现方案(支持子控件)2

QWidget窗口抗锯齿圆角的一个实现方案&#xff08;支持子控件&#xff09;2 本方案使用了QGraphicsEffect&#xff0c;由于QGraphicsEffect对一些控件会有渲染问题&#xff0c;比如列表、表格等&#xff0c;所以暂时仅作为研究&#xff0c;优先其他方案 在之前的文章中&#…

昇思学习打卡-10-ShuffleNet图像分类

文章目录 网络介绍网络结构部分实现对应网络结构 模型训练shuffleNet的优缺点总结优点不足 网络介绍 ShuffleNet主要应用在移动端&#xff0c;所以模型的设计目标就是利用有限的计算资源来达到最好的模型精度。ShuffleNetV1的设计核心是引入了两种操作&#xff1a;Pointwise G…

国内采用docker部署open-metadata

背景 最近看看开源的元数据管理项目&#xff0c;比较出名点的有open-metadata、datahub、OpenLineage、atlas。 open-metadata有1千多的贡献者&#xff0c;4.8K的stars&#xff0c;社区现在也比较活跃&#xff0c;支持的数据库类型还蛮多&#xff0c;基本市面上常见的都有支持…

【每日一练】python三目运算符的用法

""" 三目运算符与基础运算的对比 """ a 1 b 2#1.基础if运算判断写法&#xff1a; if a > b:print("基础判断输出&#xff1a;a大于b") else:print("基础判断输出&#xff1a; a不大于b")#2.三目运算法判断&#xff1a;…

揭秘IP:从虚拟地址到现实世界的精准定位

1.IP地址介绍 1.内网 IP 地址&#xff08;私有 IP 地址&#xff09; 内网 IP 地址&#xff0c;即私有 IP 地址&#xff0c;是在局域网&#xff08;LAN&#xff09;内部使用的 IP 地址。这些地址不会在公共互联网中路由&#xff0c;因此可以在多个局域网中重复使用。私有 IP 地…

股票Level-2行情是什么,应该怎么使用,从哪里获取数据

行情接入方法 level2行情websocket接入方法-CSDN博客 相比传统的股票行情&#xff0c;Level-2行情为投资者打开了更广阔的视野&#xff0c;不仅限于买一卖一的表面数据&#xff0c;而是深入到市场的核心&#xff0c;提供了十档乃至千档的行情信息&#xff08;沪市十档&#…

STM32第十六课:WiFi模块的配置及应用

文章目录 需求一、WiFi模块概要二、配置流程1.配置通信串口&#xff0c;引脚和中断2.AT指令3.发送逻辑编写 三、需求实现代码总结 需求 完成WiFi模块的配置,使其最终能和服务器相互发送消息。 一、WiFi模块概要 本次使用的WiFi模块为ESP-12F模块&#xff08;安信可&#xf…

【LLM第8篇】Delta Tuning

如何对large-scale PLM进行调整呢&#xff1f; 一个有效的方式是delta tuning&#xff1b;只更新PLM中的一小部分参数&#xff0c;其它参数不动。 把解决任务的能力具象化成delta object这样的参数&#xff0c;只需要几十兆参数存储。 过去模型参数是随机的&#xff0c;现在预…

【MySQL】逻辑架构与存储引擎

一、逻辑架构 1、MySQL逻辑架构 我们可以根据上图来对sql的执行过程进行分析 第一步&#xff1a;客户端与服务器建立一个连接&#xff0c;从连接池中分配一个线程处理SQL语句第二步&#xff1a;SQL接口接受SQL指令第三步&#xff1a;如果是5.7版本&#xff0c;就会先去缓存中…

Python字符串处理常用的30种操作

我们平时编写代码时&#xff0c;经常需要对字符串进行处理&#xff0c;本文详细介绍Python处理字符串常用的30种操作&#xff0c;并给出了对应的代码。 分割 使用split()方法将字符串按照指定的分隔符进行分割。 s "Hello,World" result s.split(","…

国产AI芯片被撕下遮羞布,宁买阉割八成性能的NVIDIA,也不买国产

曾经有传言指有国产AI芯片大受欢迎&#xff0c;还卖出了100万片&#xff0c;不过半年多时间过去&#xff0c;海外分析机构指出国内的互联网企业纷纷抢购NVIDIA阉割八成性能的H20&#xff0c;至于国产AI芯片则不获欢迎。 导致如此结果&#xff0c;在于NVIDIA拥有许多独特的优势&…