给我一首歌的时间,带你了解线程安全和死锁(2)

举报
bug郭 发表于 2022/09/30 22:56:00 2022/09/30
【摘要】 大家好,我是bug郭,一名双非科班的在校大学生。对C/JAVA、数据结构、Linux及MySql、算法等领域感兴趣,喜欢将所学知识写成博客记录下来。 希望该文章对你有所帮助!如果有错误请大佬们指正!共同学习交流作者简介:CSDN java领域新星创作者blog.csdn.net/bug…掘金LV3用户 juejin.cn/user/bug…阿里云社区专家博主,星级博主,developer.a...

大家好,我是bug郭,一名双非科班的在校大学生。对C/JAVA、数据结构、Linux及MySql、算法等领域感兴趣,喜欢将所学知识写成博客记录下来。 希望该文章对你有所帮助!如果有错误请大佬们指正!共同学习交流

作者简介:

线程不安全原因

  • 线程是抢占性执行的,线程间的调度充满了随机性(线程不安全的根本原因)

  • 多个线程对同一个资源进行了更改操作(如果是不同的资源,或者只是对一个资源进行读操作就不会出现线程不安全问题)
    我们可以更改代码结构,让不同的线程对不同的变量进行更改就不会出现问题

  • 针对变量的操作不是原子的!
    因为对变量的操作的指令并不是一条!而是多条指令!
    我们可以通过加锁操作,使多个指令打包成一个原子,避免线程不安全问题!

  • 内存可见性
    什么是内存可见性呢?
    就是编译器对cpu操作的优化!
    举个简单的例子:
    当一个线程一直循环读一个数据时,我们的cpu就要一直在内存中读数据,而我们知道内存读取的速度想必于cpu中的寄存器慢了好几个数量级!那么这时编译器就进行优化,他懒得去内存中读取数据了,直接将数据保存在寄存器进行读取操作,而如果这时有另外一个线程对该数据进行修改,那么就会产生线程不安全问题!!!
    我们的编译器都是由大佬编写的,所以在不改变逻辑性和结果的情况下,会对代码进行优化!!!
    如何避免该问题呢?
    我们可以采用synchronized关键字对线程加锁或者使用volatile关键字保证内存可见性!

  • 指令重排序导致线程不安全问题,这也是由于编译器的优化操作而导致的线程不安全问题!
    我们的代码先后执行顺序有时候并不会影响我们的结果,那么这时编译器在不改变代码逻辑的基础上就会改变一下顺序,提高运行效率,而这个操作在多线程往往会出现线程不安全问题!
    这里也可以使用synchronized关键字避免指令重排列!

synchronized关键字

我们已经了解到了线程不安全问题可以用java提供的synchronized关键字来避免!
我们来学习synchronized如何使用!

  • 修饰实例方法
class Count{
    int count=0;
    //对实例方法进行加锁
    synchronized public void increase(){
        count++;
    }
}

这里的加速操作就是相当于对该实例的对象(this)进行加锁,而代码底层又是如何完成这个加锁操作的呢?
我们知道一切对象的父类都是Object类,而我们创建一个类,除了有我们描述的基本属性外,java还会自动开辟一块空间保存对象头信息!显然我们没有听说过,这里的属性是给jvm使用的!我们程序员并没有用!而加锁就是在对象头设置一个锁的标志位!
如果多个线程对同一个锁进行操作就会有锁竞争
对不同的锁进行操作就不会出现锁竞争!

  • 修饰代码块
    java可以在任意位置加锁!,但修饰代码块时我们需要指明加锁的对象!
class Count{
    int count=0;
    public void increase(){
        synchronized(this){//指明加锁对象!
            count++;
        }
    }
}
  • 修饰静态方法
class Count{
    static int count=0;
    public static void increase(){
        synchronized(Count.class){//修饰静态方法!
            count++;
        }
    }
}

synchronozed修饰静态方法时,并不能对this加锁,因为这是类方法!
我们可以采用反射的反射对类对象进行加锁!!!

死锁

死锁类型

  • 一个线程一把锁
    我们知道synchronized关键字可以给对象加锁!那如果我们不小心给同一个对象加锁两次,会出现什么情况呢?
class Count_1{
    private int count=0;
    synchronized public void increase(){ //外层锁
        synchronized (this){ //内层锁
            count++;
        }
    }
}

假设synchronized是不可重入锁,就是不能进行多次加锁!

我们的外层锁,在进入方法后就会对该对象进行加锁,有效加锁!而里层锁,会一直阻塞等待外层锁释放锁,才会进行加锁,此时代码就阻塞在这里了! 而外层锁要方法执行结束,才能释放锁!
显然现在的情形是谁也不让着谁!这就导致了一个尴尬的局面,就是我们所说的死锁!

就好比生活中的例子:

你手机没电了,要先老板借个充电宝!老板说,你先付钱我就借你,而你的手机已经关机了,又没带现金,然后你说,你借我我就付钱!然后就两个阿叉棍在哪死锁了!!!

不过我们写jvm的大佬设计时,将synchronized设置成了可重入锁!
多次加锁并不会发生死锁!

加锁: 如果我们现在有一个线程t1 ,对象a加锁后,t1线程拿到了该锁! synchronized就会在锁信息中记入该线程信息,还有标志该线程的加锁次数为1,如果t1线程再次对a加锁,那么并不会真正的再次加锁,只会把加锁次数加一!
解锁:如果该线程解锁,锁信息就会将锁次数减一,直到锁次数为0,此时该线程就将该锁释放!

显然jvm这样可重入锁设置,如果我们多次对一个对象加锁,会我们的运行速度降低,但是这样提高了我们人力成本,如果为不可重入锁,但造成死锁,那该程序就会中断,我们需要花大量时间进行调试!!!

  • 两个线程两把锁
    假设一种情况:
    当有两个人手机需要充电,而一个人有充电头,一个人有数据线,然后想要充电的话,需要两者结合,然后两个人都比较倔,谁都不肯退让,这就造成了死锁!

  • N个线程M把锁
    这里就需要讲到教科书上的经典案例:哲学家就餐问题
    假如有一群哲学家围在一个圆桌上干饭,然后他们干饭的时候还会思考人生,思考人生时不那筷子吃饭! 他们吃饭和思考人生是随机的!
    假如有5个人,5根筷子! 显然筷子不够!但是就将用!
    每个哲学家的两边都分别有一根筷子!

情形如下:
在这里插入图片描述
而且哲学家都比较倔,当他们拿到一根筷子时,如果没有筷子了,他们会一直等待,直到又一双筷子,才会干饭!

如果哲学家同时拿一根筷子时,就会造成死锁!
这顿饭估计永远都结束不了!!!

如何解决上面的问题呢?
我们可以先个筷子编个号,然后让哲学家们约定好,先拿编号小的筷子,再拿编号大的筷子,如果编号小的筷子被拿了,那就一直等待!
在这里插入图片描述
这样一约定,哲学家就可以将这顿饭干完了!

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。