前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题

【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题

作者头像
用户11288949
发布2024-09-27 08:21:44
40
发布2024-09-27 08:21:44
举报
文章被收录于专栏:用户11288949的专栏

📚️1.引言

OK啊!!!小伙伴们,本小编又带来了一个重磅知识,我们上期讲解了关于线程安全问题,引出了加锁这个概念;但是加锁会产生一个严重的问题,就是当我们运用不当时,进行加锁会导致死锁的发生,那怎样才会导致死锁呢?以及如何避免呢?这就是小编本期的重要内容;

发车发车gogogog~~~🥳🥳🥳;

且听小编讲解,包你学会!!!

📚️2.可重入锁

2.1概念

什么是可重入锁呢???,让我们看看以下代码:

代码语言:javascript
复制
 public static void main(String[] args) {
        Object lock=new Object();
        //可重入锁实例
        Thread t1=new Thread(()->{
            synchronized (lock){
                synchronized (lock){
                    System.out.println("Hello thread");
                }
            }
        });
        t1.start();        
    }

对于如何进行加锁操作,小编上期有讲,不清楚的小伙伴可以自己去看看哦~~~

开始认知:这里由于lock已经被加过一次锁了,那么接下来再加一次锁,不会发生线程阻塞吗,第一次加又没有进行释放;

注意:上面这种理解完全是错误的,这里就是由于使用同一个线程,此时的锁对象,就能够知道第二次加锁的线程,是持有锁的线程,那么在第二次加锁时,就直接通过,就不会发生“阻塞”现象

这种特性叫:可重入性,这个锁就叫做可重入锁~~~

2.2原理理解

在Java中实现可重入锁是非常简单的,因为synchrinized自带这个特性,那么这个特性的内部原理是啥呢,且看如下图所示:

注意:对于重入锁来说,最主要两部分第一个就是对于加锁的线程是否为同一个线程,第二就是对于加锁过程的计数器的次数理解;

以上都是在java中synchronized封装实现的,那么在C++中就没有重入锁的概念,此时当存在复杂的调用关系的时候,就会存在卡死的情况,就是“死锁”,接下来就注重“死锁”的理解;

📚️3.死锁

在之前讲解过,加锁可以解决线程安全问题,但是操作不当会产生“死锁”的情况;

3.1产生死锁的情况
1.一个线程,一把锁

即在上述讲解过程中的可重入所情况,但是如果没有可重入这个性质,那么连续对一个线程加锁两次,那么就会产生死锁;

2.两个线程,两把锁

即有两个线程,当线程1加上锁A,线程2加上锁B,那么然后在两个锁不进行释放的前提下,双方都想拿到对方的锁,此时就会发生死锁的情况;

这里有代码进行演示:

代码语言:javascript
复制
Object A=new Object();//对象A
        Object B=new Object();//对象B
        //创建线程t1拿到锁
        Thread t1=new Thread(()->{
            synchronized (A) {
                System.out.println("线程t1拿到了锁A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }//线程进入休眠,保证另一个线程也能够拿到锁

                //尝试拿到对方的锁,此时锁A没有释放
                synchronized (B) {
                    System.out.println("线程t1拿到了两把锁");
                }
            }
        });

此时小编设置了两个对象,即两个锁对象,那么我们就进行线1的加锁,此时当我们拿到锁A后,在不解开锁的情况下进行另外一把锁B的获取:

代码语言:javascript
复制
Thread t2=new Thread(()->{
            synchronized (B) {
                System.out.println("线程t2拿到了锁B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }//线程进入休眠,保证另一个线程也能够拿到锁

                //尝试拿到对方的锁,没有释放自己的锁
                synchronized (A) {
                    System.out.println("线程t2拿到了两把锁");
                }
            }
        });

此时,我们就行第二个线程的实现,和上述线程1一样,当拿到自己的锁之后,在不解开锁的情况下进行锁A的获取,然后两个线程启动之后的结果就是:

此时可以发现,线程各自拿到自己的锁之后,就直接“卡住”了,这就发生了线程安全问题;

当我们打开jconsole后,可以看看我们的线程情况:

这是我们的两个对应的线程名字,此时两个线程的执行状态就如下图所示:

注意:此时可以看到线程1处于BLOCKED状态,并且在等锁B,那么锁B的拥有者是线程2;同理,线程2在等锁A,而锁A的拥有者就是线程1;

可以发现此时两个锁都在等对方释放锁,此时就产生了死锁;

3.N个线程,M把锁

此时这种情况就是要考虑到“哲学家就餐问题了”,什么是哲学家就餐问题呢???

解释:此时有5个哲学家要吃面,但是筷子只有5根,这无根筷子在每个哲学家之间,此时就是五个哲学家就是五个线程,筷子就是锁,当每个哲学家拿到筷子后,旁边的两哲学家是吃不到的,就处于阻塞的状态,一般情况下哲学家啥时候吃到面是一个随机问题,一般情况下这是没有问题的~~~

注意:当我们每个哲学家左手拿起筷子时,可以发现此时每个哲学家都吃不到面(吃面要两根筷子),都等待另一个哲学家释放筷子(锁),此时就发生了线程的阻塞 ;

3.2解决死锁的方法

在了解线程的解决死锁之前我们要知道产生死锁的必要条件

(重点) 1.互斥使用:当一个线程获取到锁之后,另一个线程也想要获取,那么此时就要进行阻塞 2.不可抢占:当一个线程获得锁之后,其他线程想要获取此时就要等到锁的释放,不能强行占用 3.请求保持:当一个线程获得锁A之后,尝试再次获取锁B(锁A是没有释放的) 4.循环等待/环路等待

以上就是死锁形成的必要条件,缺一不可~~~;

那么针对以上死锁的产生条件,第三个条件是根据具体的代码来进行实现的,但是我们可以根据最后一个来进行攻破;

注意:解决哲学家问题关键:针对五把锁我们可以对其进行编号,然后每个哲学家也进行编号,此时约定,每个哲学家开始只能够拿编号比自己小的锁,然后再拿比自己编号大的锁;

为啥能够解决死锁问题呢???且看下图所示:

过程解释:当我们为这个线程和锁进行编号后,此时由于只能拿比自己编号小的筷子,那么2号哲学家拿1筷子,3号哲学家拿2号筷子.......到最后,5号哲学家拿4号筷子,此时就可以发现多了一双筷子,那么5号哲学家先再拿起5号筷子,此时当5号哲学家吃完后,放下筷子,一次类推,可以保证每个哲学家都拿够吃到面~~~,同理这就解决了死锁这个问题;

那么此时我们就可以改变之前这个双方获取两个锁的这个代码,实现这个代码的可行性:

代码语言:javascript
复制
Thread t2=new Thread(()->{
            synchronized (A) {
                System.out.println("线程t2拿到了锁A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }//线程进入休眠,保证另一个线程也能够拿到锁

                //尝试拿到对方的锁,没有释放自己的锁
                synchronized (B) {
                    System.out.println("线程t2拿到了两把锁");
                }
            }
        });

注意:这里小编只改了第二个线程的获取锁的顺序,即可保证两个线程都能够拿到锁;

解析:这里能够执行原因:当两个线程启动的时候,线程1获取到了锁A,所以此时线程B规定先不获取锁,即他以获取锁A来发生阻塞,当线程1执行完后,线程2就能够得到两个线程了~~~,这就是引入了加锁顺序规则~~~

总结:

解决死锁有很多办法,以下是一些小编总结的一些方法:

1.添加“筷子”; 2.去掉一个线程 3.引入计数器,规定最多同时几个人吃面 4.引入加锁顺序规则 5.“银行家算法”

1~3:虽然能够解决这个问题,但是普适性不高;

4:是小编推荐的,普适性高,而且容易实现;

5:是可以解决这个死锁问题,但是不推荐,实现过程很复杂,理论成立,现实不行;

📚️4.内存可见性

4.1内存可见性实例

内存可见性问题:即一个程序读,一个程序写的过程中产生的线程安全问题;

小编就用代码实例来演示:

代码语言:javascript
复制
public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (flag==0){
                //不输出任何;               
            }
                System.out.println("flag的值进行了改变");
        });
        Thread t2=new Thread(()->{
            System.out.println("输入一个flag的值");
            Scanner scanner=new Scanner(System.in);
            flag=scanner.nextInt();
        });
        //起启动线程
        t1.start();
        t2.start();
    }

解释:此时我们规定线程1进行读的操作,若flag是0,那么就不会打印任何日志,此时小编在线程2上进行改变,线程1中flag的值,让其不满足条件,实现跳出循环,结束线程;但是输出如下:

可以发现在小编输入1,改变flag的值之后,回车并没有输出“flag的值进行了改变”,所以此时就发生了线程安全问题,即内存可见性问题~~~

4.2内存可见性原理

这里从核心指令入手:

代码语言:javascript
复制
while (flag==0){
   //不输出任何;               
}

注意:这里的核心指令有两条

1.load读取内存当中的指令到寄存器中

2.寄存器拿着值与0进行比较

那么此时就是在线程2启动到输入这个操作,不断进行循环读取,比较的过程,所以这个操作有两个关键要点:

1.load不断从内存中读取数据到CPU寄存器上,这个操作的执行结果是一样的,几秒之内已经很多次了~~~

2.load从内存中读取数据这个操作开销远远大于寄存器比较这个操作~~~

此时就出现了一个问题:编译器优化代码这个操作,即JVM在优化中发现读取数据操作一直不变,那么优化后即将这个load读取数据操作给省去了(关键原因);

代码优化:即JVM在保持原有代码逻辑不变的情况下,实现提高代码的效率,单线程还好,但是多线程很容易发生误判~~~

4.3内存可见性解决
1.进行线程休眠

核心:在上述讲解中,是因为读取这个内存的次数过多,且没有改变,所以我们能够实现,读取次数减少的操作;

代码实现如下:

代码语言:javascript
复制
Thread t1=new Thread(()->{
            while (flag==0){
                //不输出任何;
                try {
                    Thread.sleep(1000);
                }
                catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
                System.out.println("flag的值进行了改变");

        });

小编只需要在读取数据这个操作实现休眠即可;

注意:这里的休眠是为了减少从内存中读取数据到CPU寄存器上,让load开销减少,减少迫切优化的程度;此时JVM就不会进行优化了,那么就不会出现线程安全问题

2.添加volatile关键词

volatile作用:这里的volatile关键词会阻止JVM对程序进行优化,确保每次循环都会从内存中读取数据到寄存器当中~~~

volatile核心作用:解决内存可见性问题,和禁止指令重排序~~~

代码演示:

代码语言:javascript
复制
public  volatile static int flag=0; //加上volatile实现代码优化的消除
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            while (flag==0){
                //不输出任何;                
            }
                System.out.println("flag的值进行了改变");

        });

注意:volatile和上述休眠作用基本一致,都是使JVM优化程序关闭,保证每次循环都是从内存中读取数据,而不是优化成直接从寄存器当中读取数据~~~

📚️5.总结

💬💬本期小编总结了关于多线程的重要知识即死锁,分别从造成原因和如何进解决提出了关于小编的理解,以及线程安全问题之内存可见性问题,并附上了代码供小伙伴们参考参考~~~

🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

😊😊 期待你的关注~~~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-09-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 📚️1.引言
  • 📚️2.可重入锁
    • 2.1概念
      • 2.2原理理解
      • 📚️3.死锁
        • 3.1产生死锁的情况
          • 1.一个线程,一把锁
          • 2.两个线程,两把锁
          • 3.N个线程,M把锁
        • 3.2解决死锁的方法
        • 📚️4.内存可见性
          • 4.1内存可见性实例
            • 4.2内存可见性原理
              • 4.3内存可见性解决
                • 1.进行线程休眠
                • 2.添加volatile关键词
            • 📚️5.总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档