首页 > 其他分享 >线程安全是什么问题?如何引起?死锁是啥?如何解决?

线程安全是什么问题?如何引起?死锁是啥?如何解决?

时间:2024-08-24 21:53:50浏览次数:13  
标签:加锁 t1 如何 死锁 线程 内存 JVM

目录

一、什么是线程不安全?

二、如何引起的线程安全?怎么解决?

1)CPU调度执行是随机的,抢占式执行(根本原因,硬件层面咱们无法干预)

2)多个线程,对同一变量进行修改

3)修改操作中, 不是“原子”的(重点)

死锁是啥,怎么引起的?  (重点)

4)内存可见性

5)指令重排序 

 总结-保证线程安全的思路


一、什么是线程不安全?

 线程不安全:

是多个线程并发执行,而产生的结果和我们预期的不相同,这种bug是由多线程引起的,所以我们叫线程安全问题,也就是线程不安全。

 比如看下列代码:

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

我们预期是t1线程50000次,t2线程50000次,加起来一共100000次。这个是我们预期的结果。但是真实的情况如下: 

是79506次,而且每一次的结果都不一样,这样和预期不符合,就是线程安全问题。

 为什么会出现这样的情况呢?如果是单线程会如此吗?请看下文:

二、如何引起的线程安全?怎么解决?

 上面那个count的例子来解释:

我们知道CPU把线程调度上去执行,最小单位是指令,我们count++其实是3个指令:

1)把值从内存中读取到CPU的寄存器上(load)

2)在寄存器上把count+1(add)

3)把寄存器加完之后的值传到内存中(save)

当然上面那样,是我们最理想,也是我们想要的状态,但是在多线程的加持下,因为CPU的调度是抢占式的,是随机的,我们并不能操控,可能刚执行了一半就给调走了。我们基本上很难遇到上面的情况,可能会出现以下的情况(简单列举了几种):

最小单位是指令,CPU不会执行半个指令,会给你执行完一个,但是count++有3个指令,我也不知道CPU什么时候调度走,如何给别人执行,这样就会造成bug,也就是我们说的线程安全问题!

既然我们明白了什么是线程安全,下面是五种会出现线程安全问题的原因,我们可以思考下面五种原因,从而去解决线程安全问题,或者去避免写出错误的代码:

1)CPU调度执行是随机的,抢占式执行(根本原因,硬件层面咱们无法干预

CPU的调度执行具体的可以参考我往期的博客,这里就不赘述了。

2)多个线程,对同一变量进行修改

这个行不行,具体要看业务需求,但是对于Java来说,这并不是一个很好的解决方法,业务还有更好的方法。

如果是单个线程,对一变量修改,那就没有关系,不会出线程安全的BUG。

3)修改操作中, 不是“原子”的(重点)

 这个来说,难道就是把类似像count++这类不是原子的,变为原子的吗?答:是的。

那咱们要怎么变啊?一条代码写多条?

答:给它加锁! (相当于一夫一妻制,锁上其他线程就进不来了,只能等他释放锁,其他线程才能进去)

 如何使用锁?语法是啥?

关键字:synchronized

Object locker=new Object();
synchronized(locker){....}

 如图

也就是: 既然大家都是串行化了,那效率会不会大大降低呢?

答:不会,因为这只是小部分需要加锁,其他代码都是不需要的。也就少了那么一点点,对于计算机来说,这些都是小case

注意事项:

1. 这个得都是同一个锁对象(才会发生锁竞争,就是堵塞),也就是传参(我这写为locker),如果一个是locker1一个是locker2,相当于2个厕所,互不影响,都能上厕所。

2.传参(locker)只要不是int,char这种简单类型,其他object子类都行,甚至写class对象都行

3.想要相互不影响的线程必须都得加锁,就是t1和t2的count都得加锁,相当于限制t1和t2只能上这个厕所,有一个人就得排队。如果只是一个加锁,t1打包好的3条指令很大程度会插入t2那3条指令中间。(如上图)

4.如果是多个线程一起加锁,t1执行完了,t2和t3谁先执行呢?是随机执行,抢占式调度。

5.synchronized还可以修饰方法

6.还可以修饰静态的方法,但是要用类对象做锁对象

7.用锁要考虑清楚,不然到时候某个线程堵塞了,什么时候恢复就不知道了

8.可重入锁,可连续加锁两次/多次,遇到加锁的会放行,不加第二次。(请看下面关于死锁的内容)

死锁是啥,怎么引起的?  (重点)

Ⅰ:针对一把锁连续加锁两次

如上图,如果连续加两次锁,那么就会构成一个死循环,也就成了死锁。

但是要注意的是,在Java中synchronized可以自动判断,如果有加锁过了,是同一个锁对象的话,则在下次加锁的时候会放行,这也叫可重入锁。不会真正的报错,抛异常。 

Ⅱ:两个线程两把锁

1)线程1在对A加锁,线程2对B加锁,

2)线程1在不释放A的情况下,要对B加锁;同时线程2在不释放B的情况下,对A加锁。

依照上面的情况,势必会僵持住,造成死循环,这也是死锁。

比如:

我和朋友在吃饺子,有醋有辣椒可以蘸饺子,我想拿醋放了点,朋友拿了辣椒放了点。这时候我又想要辣椒了,他又想用醋。这时他说你把醋给我,我再把辣椒给你。我说你先给我辣椒,我在给你醋。这时候我们就会僵持不下(当然这件事情可以商量,但是在程序中,代码的执行是死板的,就会变成死锁)

上述代码就是个例子。t1不释放locker1又要locker2,t2不释放locker2又要locker1,两个线程谁也不服谁。

Ⅲ:M个线程M把锁

和2个线程2把锁类似。哲学家都在等右手边的筷子,才能拿起筷子吃面条,但是没人放下右手边的筷子(因为他们也在等他们右手边的筷子才能吃完,吃完才能放下来)都僵持住了。

死锁的四个必要条件:

(1)锁是互斥的(锁的基本特性)

(2)锁是不可被抢占的(锁的基本特性)

比如线程1加锁了,没释放的前提下,线程2不能去抢

(3)请求与保持(代码结构)

比如线程1加锁A了,没释放A的前提下(在保持着),去加锁B(请求)

(4)循环等待/环路等待/循环依赖

多个线程获取锁,就可能会陷入死循环

由于1和2是锁的基本特性,所以我们无法避免。但是对于3来说,有些时候,我们是需要写成3那样的结构的。所以就要看4,只要4不成立,就不会产生死锁。 

如何用4解决呢?

答:约定加锁的顺序,编号小的先加锁,如何编号大的再加锁

比如上面的代码,我们先让t1加锁locker1和locker2,再让t2加锁locker1和locker2。这样按顺序加锁就不会产生死锁了。

4)内存可见性

请看下图:

写了2个线程,想要t2输入值修改了n之后,t1的while中结束,因为n不为0了。但是我们输入后会发现并不会结束。为什么?这就是因为内存可见性  

内存可见性同时也是bug,也是线程安全问题。但这并不是我们代码书写,而是JVM自己的问题,是JVM自己优化,优化出了问题。

这里的快慢并不是绝对速度的快慢,而是相对速度的快慢:

内存不可见性原因及原理:

JVM发现执行这个代码的时候,发现每次循环的过程中,执行1)操作,开销非常大,而且每次执行1)的结果都是一样的。

并且JVM根本没有意识到,用户未来会修改这个n,于是JVM就做了一个大胆的操作,直接把1)这个操作优化了。

也就是每次循环,不会重新读取内存中的数据,而是直接读取寄存器/cache中的数据。当做了这个操作后,循环的开销也就大大降低了,

但是t2对于在内存中的修改,t1是感知不到的,所以也就是t1的在“内存不可见性”。

为啥JVM要优化代码呢?

答:因为程序员的编程水平是参差不齐的,为了减少程序员的差距,降低程序员的门槛,JVM就会优化代码,让厉害的程序员和一般的程序员不会差距太明显。 

如何解决内存可见性问题?

加入关键字:volatile,也就是在n那里加入volatile

 在n上面加个volatile就能解决这个问题,volatile(易变的),也就是告诉JVM这个n是易变的,让JVM不要优化这个代码,让它在内存的修改能被读取到。 t1也就不会直接在寄存器上读取了。

5)指令重排序 

 指令重排序也是JVM自己优化出来的一个bug

比如你妈妈让你去菜单给你个清单,如下 

如果你老老实实按清单的买,那就太慢了,而且要走很久,如果你调整一下顺序,则就能快很多,那么JVM也是这么想的,把指令重新调整一下,提高效率。这就叫做指令重排序。

这个是一个单例模式中懒汉模式的代码,一个程序只需要一个对象,所以如果没有创建,我可以创建出来一个,如果有了,我返回这个对象就行了。两层if是不一样的功能,第一层主要是为了不频繁加锁消耗资源(因为如果instance没被new出来,则需要加锁给new出instance,不然可能会被其他线程打断)。第二层是实现唯一的实例,new过就直接return就好了。

为什么这个代码也算线程不安全呢?

 我们的JVM在执行new操作的时候,会把2和3调整一下

 既然这个问题这么可怕,我们怎么解决这个问题呢?

答:用关键字volatile。

只要我们加上volatile,就告诉JVM这个instance是易变的,让JVM不要去优化它,这样就可以解决指令重排序的问题了。

 总结-保证线程安全的思路

1. 使⽤没有共享资源的模型

2. 适⽤共享资源只读,不写的模型

        a. 不需要写共享资源的模型

        b. 使⽤不可变对象

3. 直⾯线程安全(重点)

        a. 保证原⼦性

        b. 保证顺序性

        c. 保证可⻅性

标签:加锁,t1,如何,死锁,线程,内存,JVM
From: https://blog.csdn.net/2301_80958683/article/details/141363092

相关文章

  • 如何实现一棵红黑树
    目录1.什么是红黑树2.红黑树的实现2.1红黑树的插入新插入的结点应该是什么颜色的呢?插入情况的分析​编辑插入代码如下所示2.2红黑树的查找2.2检测红黑树1.什么是红黑树?红黑树是一棵接近平衡的二叉搜索树。由于AVL树在频繁大量改变数据的情况下,需要进行很多的旋转......
  • [Java基础]虚拟线程
    虚拟线程(VirtualThread)是JDK而不是OS实现的轻量级线程(LightweightProcess,LWP),由JVM调度。许多虚拟线程共享同一个操作系统线程,虚拟线程的数量可以远大于操作系统线程的数量。虚拟线程和平台线程有什么关系?在引入虚拟线程之前,java.lang.Thread包已经支持所谓的平台线程(P......
  • 【C语言】进程和线程详解
    目录C语言进程和线程详解1.进程和线程的对比2.进程的基本概念2.1进程的定义2.2进程的特点2.3进程的生命周期3.进程管理3.1进程创建3.2进程间通信(IPC)3.2.1管道(Pipe)4.线程的基本概念4.1线程的定义4.2线程的特点5.POSIX线程库5.1引用头文件5.2创建线程......
  • [操作系统]死锁
    死锁死锁是指在并发系统中,两个或多个进程因为互相等待对方释放资源而无法继续执行的状态。死锁发生的条件通常包括以下四个条件:互斥条件(MutualExclusion):至少有一个资源被标记为只能被一个进程占用,即一次只能有一个进程使用该资源。请求与保持条件(HoldandWait):一个进程在持......
  • C#面:在 MVC 中如何执行 Windows 认证?
    在MVC中执行Windows认证可以通过以下步骤实现:在Web.config文件中启用Windows身份验证。找到<system.web>节点,并确保已经添加或者设置。在Controller的Action方法上使用[Authorize]属性来限制只有经过Windows身份验证的用户才能访问该Action方法。在View......
  • 如何缩短微信文章链接长度
    有时候,我们想把微信公众号的文章发到其他平台上,这时候就需要复制文章的链接。‍手机端复制方式如下:​‍微信对于短网址的优化以前,微信公众号文章的链接特别长。但在2016年末,微信悄悄做了更新,链接网址变短了许多:​此时两个网址都能打开同一篇文章。这是个小小的改变,但非......
  • 【AI绘画基础入门】如何体验AI绘画工具Stable Diffusion,附SD安装教程
    大家好,我是SD教程菌。专注于AI绘画技术干货分享。需要AI绘画学习资料的可以文章底部可扫码免费领取。期待与你一路同行,共同成长。关于如何使用StableDiffusion工具,一直是很多小伙伴经常咨询的问题之一。今天就和大家一起聊聊关于如何本地安装部署StableDiffusion。本......
  • 黑神话悟空 PC端配置需求详解:如何为不同游戏体验选择合适的配置?
    《黑神话:悟空》是一款备受期待的动作角色扮演游戏,由游戏科学(GameScience)开发,基于《西游记》改编。随着游戏的发布,许多玩家都在关心一件事:我的电脑能带动这款游戏吗?本文将详细介绍《黑神话:悟空》的最低配置和终极体验配置,并探讨不同配置的选择理由。最低配置需求:1080p中等画质......
  • 学编程的普通人如何通过技术变现,副业月入过万?python兼职,学习
    前言我有一个朋友,在国企工作,月薪一万出头。前几个月他和我说,他辞职了。说实话在这种行情下,敢裸辞的都是勇士,我问他为啥要辞职,他说现在他的副业已经超过主业收入了,上班反而耽误他挣钱,他光靠做副业,最高一个月收入6w+,这比上班香多了,时间还自由。说这个并不是主张让大家辞职,而......
  • 风力发电机组的控制系统如何确保安全?
    风力发电机组的控制系统通过多个方面来确保安全,主要包括:1.硬件安全装置:防雷装置:风力发电机组通常设置在较高地势,易受雷击。因此,设置避雷针、避雷带、接地装置等防雷装置,以有效防止雷电对设备的损坏。温度保护:防止设备过热引发火灾等安全事故,当设备温度超过设定阈值时,自动切......