首页 > 其他分享 >关于线程安全

关于线程安全

时间:2024-10-19 16:17:37浏览次数:7  
标签:加锁 synchronized 代码 安全 死锁 线程 关于 等待

 1.多线程带来的风险(线程安全)

我们首先运行下面的代码,我们明明自增了10w次结果显示为0,说明main线程先执行了打印,那该如何解决这一问题,我们可以加t.join使main线程等待,先把t1、t2执行完

 

 但是此时的结果和我们预期的结果不一样,这样的代码很明显有bug,实际执行的效果和预期的效果不同就是bug,这样的问题多线程并发执行引起的问题,如果把两个线程变成串行执行就会得到预期的结果

很明显,当前bug是由于多线程的并发执行代码引起的bug,这样的bug就称为线程安全问题或者叫做"线程不安全",反之如果一个代码在多线程的环境下,也不会出现类似上述的bug这样的代码就叫“线程安全” 

接下来我们来解析count++这条指令,这个操作看起来是一行代码实际上对应到三个CPU指令

1.load把内存中的值(count变量)读取到CPU寄存器

2.add,把指定寄存器的值进行+1操作(结果还是在这个寄存器中)

3.save,把寄存器中的值写回到内存中

cpu执行这三条指令的过程中,随时可能触发线程调度的切换,

比如12线程切换走……线程切换回来3 等等多种情况

由于操作系统调度是随机的,执行任何一个指令的过程中都可能触发上述的“线程切换”操作。

接下来我们可以通过画图的方式来模拟cpu上的操作

 错误调度方式:

 那上面的代码编译是都都是大于5w有没有可能小于5w?

答案是可能小于5w,有时候可能进行多次结果还是1,如下图所示

 当前这两个线程各自自增5w次,如果t1第一次自增过程中t2完成了5w次自增,t1还剩下49999次自增,最低我们是无法预测的,但是我们可以知道是会出现少于5w次的情况。其实不管是自增100次还是10w次,都有可能出现线程不安全的问题,只是概率的大小问题。如果循环一共100次就算不串行加入join输出的结果还是100,但是也有可能出现少于100的情况只是概率变小了。同时因为一次自增50次次数比较小,有可能在执行t2.start()之前t1就算完了,等后续t2再执行就变成串行的了。串行执行就是一个线程完全执行完成之后再执行另外一个线程。

2.线程安全问题产生的原因 

通过上面的示例我们可以知道线程安全问题产生的原因有以下几点

1.根本原因是:操作系统对线程的调度是随机的,抢占式执行的

2.多线程同时修改一个变量,抢占式执行策略最初诞生多任务操作系统的时候,非常重大的发明,后世的操作系统都是一脉相承的,如果是多个线程读取同一个变量那就没有问题,因为读取操作是读入已经操作好的数据

3.修改操作不是原子,原子性指的是不可再分的情况,如果修改操作只是对应一个CPU指令,就可以认为是原子性的。同时java中的++、--、+=、-=也不是原子性操作,但是在java中“ = ”就是原子性的操作,但是在C++中就不一定了

4.内存可见性问题引起的线程不安全问题

5.指令重排序引起的线程不安全

3.那如何解决线程安全问题呢?

1.关于系统的随机调度问题我们是无法解决的,这是操作系统的底层设定我们也左右不了

2.多个线程同时修改同一个变量的问题,这点是和代码结构相关,我们可以调整代码结构规避一些线程不安全的代码但是这样的方法并不通用,有些情况需求上就是需要多线程同时修改同一个变量。但是java中的String就是采取“不可变”特性确保线程安全问题,那String是咋实现不可变的效果的呢?String里面没有提供public的修改方法和final没有任何关系,String的fian用来实现‘不可继承’

3.关于第三点:修改操作不是原子的。Java中解决线程安全问题最主要的方案就是加锁,通过加锁让不是原子的操作打包成原子的操作,计算机中的锁和生活中的锁一样互斥排他 。一旦把锁锁上其他人想加锁就得阻塞等待,计算机中不允许暴力拆锁,只能阻塞等待。那解决上述问题我们就可以通过在count++之前加锁,之后再进行count++计算完毕之后再解锁,加锁之后执行三步的过程中其他线程就没法插入了。

2.1关于锁

加锁解锁本身就是操作系统提供的api,很多编程语言都是对于这样的api进行封装了,大多数的封装风格都是采取两个函数。在java中使用synchronized这样的关键字搭配代码块来实现效果。

关于锁在代码中的使用

上述代码加锁之后的实现:

 关于加锁我们需要注意的是:

1.两个线程针对同一个对象加锁才能产生互斥效果(一个线程加锁之后另一个线程就得阻塞等待,等待第一个线程释放锁才有机会),如果是不同的对象,此时不会产生互斥效果,线程安全问题没有得到改变。

2.线程安全问题不是你写了synchronized就可以,而是要正确的使用锁。synchronized()代码块要合适,synchronized()指定锁对象也得合适

2.2 synchronized 变种写法

这里面的锁加在this对象里面,说明是同一个对象Counter,此时也是加锁成功的。当然我们也可以把synchronized加在方法外面也是同样的效果。同时像StringBuffer、Vector这些对象方法上就是带有synchronized 也是针对this进行加锁。

 同样的效果:

 

StringBuffer内部带有Synchronized: 

 

当然这样的用法也存在一些特殊的情况,static修饰的方法不存在this,此时synchronized修饰方法,相当于针对类对象加锁

2.3关于死锁的问题

看起来是两次一样的加锁很没必要,但是在实际开发中,就很容易写出这样的代码。这样会产生阻塞等待,此时我们要等到第一次加锁释放,第二次加锁的阻塞才会接触(才能够继续执行)。一旦调用的层次比较深就会出现死锁的问题。

两次加锁:

关于死锁的定义:按照之前对锁的设定,第二次加锁的时候就会产生阻塞等待,直到第一次的锁被释放,才能获得到第二个锁,但是释放第一个锁也是由该线程完成的,结果线程躺平了,啥也不干,也就无法进行解锁这样的操作,这时就会产生死锁的现象。

但是在java中为了很好的解决这些问题,在synchronized中引入了可重入的概念,但是在c++中不具有这样的特性很容易就产生死锁的现象。

引入可重入加锁这样的概念之后,当某个线程针对一个锁加锁成功后,后续线程再次针对这个锁进行加锁,不会触发阻塞等待,而是直接往下走,因为当前这把锁就是被这个线程持有的,但是如果其他线程尝试加锁就会正常阻塞。

可重入加锁的实现原理,关键在于锁对象内部保存,当前是哪个线程持有的这把锁,后续有线程针对这个锁进行加锁的时候,对比一下锁持有者的线程和当前加锁的线程是不是同一个。

针对一个线程多次加锁的过程synchronized采用计数的方法来进行快速解锁的操作,如图所示:

如何自己实现一个可重入锁(重点)

1.在锁内部记录当前哪个线程持有锁,后续每次加锁都进行判断

2.通过计数器,记录当前加锁的次数,从而确定何时真正进行解锁

2.4关于死锁

1.前面引入可重入锁,在java中一个线程加锁多次根本不会出现死锁的现象,但是,两个线程两把锁,每个线程都获取到一把锁之后,尝试获取对方的锁,这样其实是会出现死锁的现象。如下图所示:

上述代码就是出现死锁这样的现象,什么也没打印但是也没有显示进程结束。 此时在JDK中的环境显示就会看到blocked这样的字眼,这就是因为竞争锁而产生阻塞的缘故。

同时这里的死锁是必须拿到第一把锁之后,再尝试拿到第二把锁(不能释放第一把锁),那如果上述代码不加sleep是否会出现一样的现象。如果没有sleep那么t1可能拿到了objiect1 和objiect2,这个时候t2都还没动,自然无法构成死锁。

2.接着我们来讲一下第二种死锁的现象,N个线程M把锁的现象。

此时我们可以举一个哲学家就餐的问题,一个桌子围着五个哲学家同时桌子上摆放着五根筷子和一碗面条,哲学家的状态一是思考人生(放下筷子)二是吃面条(拿起筷子),5个哲学家随机触发吃面条和思考人生,5个哲学家就相当于是5个线程,5根筷子就相当于5把锁,每个线程只需要拿到其中的两根即可,大部分情况下,上述模型可以很好的运转在一些极端的情况下会造成死锁的现象,比如同一时刻大家都想吃面条,同时拿起左手的筷子,此时任何一个哲学家都无法拿起右手的筷子,任何一个哲学家都吃不成面条,此时就出现死锁的现象。

2.5如何避免死锁

那如何避免代码中出现死锁呢?首先我们需要知道构成死锁的条件是什么从而一一化解

1.锁是互斥的(锁的基本特征),一个线程拿到锁之后另一个线程再尝试取锁必须阻塞等待

2.锁是不可抢占的,线程一拿到锁线程二也尝试拿到这个锁,线程二必须阻塞等待,线程二不能直接剥夺过来,这也是锁的基本特性

3.请求和保持,一个线程拿到锁1之后,不释放锁1的前提下获取锁2,如果先放下左手筷子再去获取右手筷子,就不会构成死锁。因此解决这种情况使用的方法就是不要去嵌套,但是这种办法的通用性不够,有些情况确实是需要拿到多个锁,再进行某个操作。

4.循环等待,多个线程多把锁之间的等待,构成了循环,A等待B,B也等待A或者A等待B,B等待C……   但是如果约定好加锁的顺序就可以破除循环等待

 2.6关于死锁的小结

1.构成死锁的场景

  • 一个线程多把锁,通过可重入锁解决
  • 两个线程两把锁互相获取对方的锁,我们需要了解如何去编写这样的代码
  • N个线程M把锁,我们可以通过举例哲学家就餐的问题

2.死锁的必要条件

  • 锁是互斥
  • 不可剥夺
  • 请求和保持
  • 循环等待

3.如何避免死锁

  • 把嵌套的锁改成并列的锁
  • 加锁的顺序做出约定

标签:加锁,synchronized,代码,安全,死锁,线程,关于,等待
From: https://blog.csdn.net/2301_80785428/article/details/142929674

相关文章

  • 深入探索ArkWeb:构建高效且安全的Web组件
    本文旨在深入探讨华为鸿蒙HarmonyOSNext系统(截止目前API12)的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。引言在HarmonyOSNext的开发环境中,Ar......
  • java_day19_线程组、线程池、定时器、InetAddress、网络编程、设计模式
    一、线程组:线程组:将属于同一类的线程划分到同一组中,可以直接对线程组进行设置。ThreadGroup构造方法:ThreadGroup(Stringname)构造一个新的线程组。代码案例:classMyThread1extendsThread{publicMyThread1(){}publicMyThread1(ThreadGr......
  • 线程的特殊方法
    一、休眠线程publicstaticvoidsleep(longmillis)classMyThread2extendsThread{@Overridepublicvoidrun(){System.out.println("我是李刚,现在开始睡觉了...");try{Thread.sleep(5000);}catch(InterruptedExcepti......
  • 安全见闻(
    在安全中脚本语言在安全中脚本语言的编写是很重要的,比如在一句话木马中要了解语言如何编写php,js之内,要去了解这些语言的原理构成,了解基础语法才能更好编写攻击脚本等。比如:macro(宏病毒)比如利用metasploit生成宏病壶,将宏病毒植入office文件中,很多公司都有office文件或者产......
  • 多种方式实现安全帽佩戴检测
    为什么要佩戴安全帽在探讨安全帽佩戴检测之前,我们先来了解下安全帽佩戴的必要性:保护头部免受外力伤害防止物体打击在建筑施工、矿山开采、工厂车间等场所,经常会有高空坠物的风险。例如在建筑工地上,可能会有工具、材料、零件等从高处掉落。即使是一个很小的物体,从高处落下时也会产生......
  • java_day18_多线程、线程安全问题、死锁、等待唤醒机制
    一、线程1、多线程进程:是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。举例:IDEA,阿里云盘,wegame,steam线程:是进程中的单个顺序控制流,是一条执行路径一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行......
  • 关于 configure 的使用
    在使用configure之前,首先要明白怎么用,configure-h查看帮助Someinfluentialenvironmentvariables:CCCcompilercommandCFLAGSCcompilerflagsLDFLAGSlinkerflags,e.g.-L<libdir>ifyouhavelibrariesinanonstandardd......
  • 国家信息安全水平考试(NISP一级)最新题库-第十四章
    目录另外免费为大家准备了刷题小程序和docx文档,有需要的可以私信获取1容灾系统可用性与指标RPO、RTO的关系是()A.RPO和RTO越大,可用性越大;B.RPO和RTO越小,可用性越大;C.RPO越大,RTO越小,可用性越大;D.RPO越小,RTO越大,可用性越大正确答案:B2一个基于特征的入侵检测系统根据......
  • 线程安全案例
    多窗口售票使用实现Runnable接口的方式实现售票问题1:我们加入了循环和延迟模拟现实生活售票的场景后发现1.出现售卖重复的票号【计算机中cpu的计算是具备原子性的】2.出现非法的票号【随机性导致的,cpu小小的时间片,足以执行很多次】上述的......
  • 网络安全学习路线+自学笔记(超详细) 自学网络安全看这一篇就够了
    一、什么是网络安全网络安全是一种综合性的概念,涵盖了保护计算机系统、网络基础设施和数据免受未经授权的访问、攻击、损害或盗窃的一系列措施和技术。经常听到的“红队”、“渗透测试”等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。作为一......