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

什么是线程安全问题

线程安全问题指的是在多线程编程环境中,由于多个线程共享数据或资源,并且这些线程对共享数据或资源的访问和操作没有正确地同步,导致数据的不一致、脏读、不可重复读、幻读等问题。线程安全问题的出现,通常是因为线程之间的并发执行导致了数据竞争(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;//保证内存可见性,禁止指令重排序

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【大数据综合试验区1008】揭秘企业数字化转型:大数据试验区政策数据集大公开!

今天给大家分享的是国内顶级期刊中国工业经济2023年发布的最新期刊《政策赋能、数字生态与企业数字化转型——基于国家大数据综合试验区的准自然实验》文章中所使用到的数据集——国家大数据综合试验区政策数据集以及工具变量数据&#xff0c;该文章基于2009-2019年中国上市企业…

两个全开源的3D模型素材下载网站源码 3D图纸模型素材 三维图形素材会员下载站源码

今天推荐两个全开源的3D模型素材下载网站源码 3D图纸模型素材 三维图形素材会员下载站源码&#xff0c;这两个源码完整&#xff0c;都是基于thinkphp内核开发的&#xff0c;框架稳定&#xff0c;带数据库&#xff0c;源码文件&#xff0c;可以直接部署使用。 第一个&#xff1a…

【数据结构与算法】快速排序挖坑法

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《数据结构与算法》 期待您的关注 ​

Redis源码整体结构

一 前言 Redis源码研究为什么先介绍整体结构呢?其实也很简单,作为程序员的,要想对一个项目有快速的认知,对项目整体目录结构有一个清晰认识,有助于我们更好的了解这个系统。 二 目录结构 Redis源码download到本地之后,对应结构如下: 从上面的截图可以看出,Redis源码一…

【密码学】信息安全五大属性

信息安全的五大属性&#xff0c;通常被称为CIA三元组加上两个额外的属性&#xff0c;他们是确保信息在存储、处理和传输过程中保持安全、完整和可用的关键要素。这些属性共同构成了信息安全的基础框架。 一、信息安全五大属性 我先给出一个直观的列表&#xff0c;方面大家后续…

BigDecimal(double)和BigDecimal(String)有什么区别?BigDecimal如何精确计数?

BigDecimal(double)和BigDecimal(String)的区别 double是不精确的&#xff0c;所以使用一个不精确的数字来创建BigDecimal&#xff0c;得到的数字也是不精确的。如0.1这个数字&#xff0c;double只能表示他的近似值。所以&#xff0c;当我们使用new BigDecimal(0.1)创建一个Bi…

69.WEB渗透测试-信息收集- WAF、框架组件识别(9)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;68.WEB渗透测试-信息收集- WAF、框架组件识别&#xff08;8&#xff09; 有无waf存在&am…

前端必修技能:高手进阶核心知识分享 - CSS 阴影属性详解

CSS 涉及设计到阴影的相关内容包括三个方面&#xff1a;box-shadow属性&#xff08;盒子阴影&#xff09;、 text-shadow属性&#xff08;文本阴影&#xff09;、drop-shadow滤镜。 本篇文章旨在详细介绍和分析三种阴影的具体参数设置和典型用例。 box-shadow属性&#xff08;…

蚂蚁全媒体总编刘鑫炜谈新媒体时代艺术家如何创建及提升个人品牌

新媒体时代艺术家如何创建及提升个人品牌形象——专访蚂蚁全媒体总编刘鑫炜 图为蚂蚁全媒体总编刘鑫炜 在新媒体风潮席卷全球的今天&#xff0c;传统艺术与新媒体技术的融合越来越紧密。这种变革不仅改变了艺术作品的呈现方式&#xff0c;也给艺术家们提供了更多的可能性。那么…

从FasterTransformer源码解读开始了解大模型(2.1)代码通读03

从FasterTransformer源码解读开始了解大模型&#xff08;2.2&#xff09;代码解读03-forward函数 写在前面的话 本篇的内容继续解读forward函数&#xff0c;从650行开始进行解读 零、输出Context_embeddings和context_cum_log_probs的参数和逻辑 从653行开始&#xff0c;会…

怎样让家长单独查到自己孩子的期末成绩?

期末考试的钟声已经敲响&#xff0c;随着最后一份试卷的收卷&#xff0c;学生们的紧张情绪渐渐平息。然而&#xff0c;对于老师们来说&#xff0c;这仅仅是另一个忙碌周期的开始。成绩的统计、分析、反馈&#xff0c;每一项工作都不容小觑。尤其是将成绩单一一私信给家长&#…

【Python】组合数据类型:序列,列表,元组,字典,集合

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️Python】 文章目录 前言组合数据类型序列类型序列常见的操作符列表列表操作len()append()insert()remove()index()sort()reverse()count() 元组三种序列类型的区别 集合类型四种操作符集合setfrozens…

分子AI预测赛Task4笔记(结束)

话不多说&#xff0c;直接上官方链接&#xff1a;‌​​​‍&#xfeff;​⁠​‌​‍​​&#xfeff;​‌​⁠‬​&#xfeff;‬​​‌​​​​‬‬​​​​‍⁠‍‌​&#xfeff;⁠Task3&#xff1a;进阶baseline详解 - 飞书云文档 (feishu.cn)Task4&#xff1a;持续尝试&…

RAID的实现

软RAID&#xff0c;在实际工作中使用较少&#xff0c;性能太次。 mdadm工具&#xff0c;主要在虚拟机上使用&#xff0c; 硬RAID 用一个单独的芯片&#xff0c;这个芯片的名字叫做RAID卡&#xff0c;数据在RAID中进行分散的时候&#xff0c;用的就是RAID卡。 模拟RAID-5工作…

【Transformer】transformer模型结构学习笔记

文章目录 1. transformer架构2. transformer子层解析3. transformer注意力机制4. transformer部分释疑 图1 transformer模型架构 图2 transformer主要模块简介 图3 encoder-decoder示意图N6 图4 encoder-decoder子层示意图 1. transformer架构 encoder-decoder框架是一种处理NL…

AI编程探索- iOS 实现类似苹果地图 App 中的半屏拉起效果

想要的效果 功能分析 想要实现这种效果&#xff0c;感觉有点复杂&#xff0c;于是就想搜一下相关资料看看&#xff0c;可问题是&#xff0c;我不知道如何描述这种效果&#x1f602;。 当我们遇到这种效果看着很熟悉&#xff0c;但是不知道如何描述它具体是什么的时候&#…

有一个日期(Date)类的对象和一个时间(Time)类的对象,均已指定了内容,要求一次输出其中的日期和时间

可以使用友元成员函数。在本例中除了介绍有关友元成员函数的简单应用外&#xff0c;还将用到类的提前引用声明&#xff0c;请读者注意。编写程序&#xff1a; 运行结果&#xff1a; 程序分析&#xff1a; 在一般情况下&#xff0c;两个不同的类是互不相干的。display函…

华为云OBS 通过S3客户端访问

华为云好像没有对S3协议的支持说明其实底层是支持S3协议的。 使用S3的时候我们会需要endpoint&#xff0c;桶名字&#xff0c;region&#xff0c;AWS_ACCESS_KEY,AWS_SECRET_KEY 其中endpoint 就是图片中的&#xff0c;桶名字也很容易找到&#xff0c;region 就是你的endpoint…

Nestjs基础

一、创建项目 1、创建 安装 Nest CLI&#xff08;只需要安装一次&#xff09; npm i -g nestjs/cli 进入要创建项目的目录&#xff0c;使用 Nest CLI 创建项目 nest new 项目名 运行项目 npm run start 开发环境下运行&#xff0c;自动刷新服务 npm run start:dev 2、…