首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【智能解析多线程:线程安全与死锁的深度剖析】

【智能解析多线程:线程安全与死锁的深度剖析】

原创
作者头像
ImAileen
发布2025-01-25 21:21:31
发布2025-01-25 21:21:31
1470
举报
文章被收录于专栏:TencentTencent

温故而知新

线程安全问题

<font size = 5>对比上面的两段代码,我们可以看到,当我们i的的值 循环次数较少时,发生线程安全的问题明显就减小了,但它任然存在线程安全问题。 <font size = 5>上面代码中当i很小时,t1就开始计算了,可能会出现:t2还没启动,t1就运行完了,此时这两个线程就相当于是串行执行。 <font size = 5><font color = red>通过对比上面两段多线程代码的运行结果:我们可以得出多线程代码运行具有随机性。

代码语言:java
复制
package thread;

class Counter2{
    private  int count = 0 ;
    void add(){
        count++;
    }
    int get(){
        return  count;
    }
}
public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        Counter2  counter2 = new Counter2();
        Thread t1 = new Thread(() -> {
            for(int i = 0 ; i < 5000 ; i++){
                synchronized (counter2){
                    counter2.add();
                }
            }
        });
        Thread t2 = new Thread(() -> {
            for(int i = 0 ; i < 5000 ; i++){
                synchronized (counter2){
                    counter2.add();
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + counter2.get());

    }
}

<font size = 5><font color = red>如果一个线程加了锁另一个线程不加锁,那线程还安全吗?

多线程中有的线程未加锁

<font size = 5><font color = red>多线程中如果有的线程未加锁,会发生线程不安全的情况,上面代码中t2未加锁,即使t1加锁了,由于t2没有任何阻塞,没有互斥,任然会使t1++到一半的时候,被t2进来把结果覆盖掉。 <font size = 5><font color = g>故事时间:上面的就好比两个男的追一个女的,但是这个女的和其它男的谈了,但是这两个男的依旧是穷追不舍,其中一个呢就比较老实一点,默默观望;另一个就是霸王硬上弓,恨不得打一架

一个线程有多把锁

<font size = 5><font color = g>假设t2先启动(t1先不考虑),t2线程第一次加锁肯定能成功,但当t2尝试第二次加锁时,此时counter2变量,属于已被锁定的状态,根据之前的知识,当我们针对一个已被锁定的线程加锁就会出现阻塞等待,并且会一直阻塞到对象被解锁时。 <font size = 5><font color = red>想要获取第二把锁,就需要先给第一把锁解锁,但是想要给第一把锁解锁就需要执行完第一层大括号,但执行完第一层大括号又需要先获取第二层锁,这两层加锁解锁操作相互矛盾,这种情况就叫做“死锁”。 <font size = 5><font color = blue>但是根据上面的运行结果,我们可以看到,这个结果是正确的,也就是说上述死锁过程对于<font color = red>“synchronized”<font color = blue>并不适用,<font color = green>但是对于C++/python 就会出现死锁的现象。

| 但是如果有N层锁,如何判定这个 } 是最外层的 } ,JVM如何识别? |

|:----|

  • <font color=black><font size = 5>解决方案⚠️:<font color=purple>“引用计数器”:<font color=black>在锁对象里面维护一个计数器(int n): - <font color=black><font size = 5>每次遇到 { ,n 就 ++ (只有第一次才真正加锁) - <font color=black><font size = 5>每次遇到 } , n 就-- (当 n 减到0了,才真正解锁) - <font color=red><font size = 5>也就是说里层加锁是虚而不实的,一般来说,只需要在外层加锁即可。【之所以要在里层加锁,是为了防止出现死锁,这种机制叫 “可重入锁”】

死锁三大场景

场景1: 非可重入锁 ——(多套几个锁即可解决死锁)

场景2: 两个线程两把锁【你在等我,我也在等你,不主动就永远没有故事直到死亡~】—— (通过jconsole调用栈+状态定位解决)

  • <font color=green><font size = 5>现有线程1和线程2,以及有锁A和B,现在线程1和线程2都需要获取锁A和锁B,但是拿到锁A之后,不释放A,继续获取锁B

<font color=blue><font size = 5> 故事时间:疫情期间,有一个地方的健康码崩了,程序员赶紧回公司去修复Bug,但他在楼下被保安拦住要求出示健康码才能上楼,但是程序员如果没有上楼修复好Bug就无法出示健康码,如果两个人你不让我我不让你,就会一直僵持,这就是死锁。

  • <font color=red><font size = 5>代码示例:
代码语言:java
复制
package thread;

public class Demo22 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();

        Thread t1 = new Thread(() -> {
            synchronized(locker1){
                //为了更好的控制线程的执行顺序,引入sleep,否则死锁可能会重现不出来
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized(locker2){
                    System.out.println("t1 获取两把锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized(locker2){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized(locker1){
                    System.out.println("t2 线程拿到两把锁");
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}
  • <font color=black><font size = 5>上面代码中,先分别让t1和t2拿到一把锁,然后再尝试去获取对方的锁。
  • <font color=red><font size = 5>根据上面的运行结果,我们可以看到进程并未退出,也未打印线程中的内容,这就是死锁现象。
  • <font color=red><font size = 5>上图是在jconsole找到阻塞的具体代码位置。并且在Java的锁对象中会记录当前是哪个线程持有这把锁。
  • <font color=green><font size = 5>如果只有一个线程,就不会触发锁竞争,也就不会发生阻塞现象。

___

场景3:N个线程,M把锁

  • 随着线程数目/锁个数的增加,此时,情况更加复杂,就更容易出现死锁。 - 案例:哲学家问题
  • 每个滑稽都坐在每个筷子之间,每个滑稽都要做两件事情: - 1.思考人生(放下筷子) - 2.吃面条(需要拿起左右两根筷子)
  • 每个哲学家啥时候吃面条,啥时候思考人生都是不确定的(抢占式执行)
  • 上面的模型,大部分可正常工作,但是如果出现极端情况就会出现问题:
  • 同一时刻,所有滑稽老铁,都拿起左边的筷子,此时,所有滑稽老铁都无法拿起右边的筷子,并且每个滑稽都是比较固执的人,每个哲学家只要吃不到面条,就不会放下手中的筷子。
  • 上面就是典型的死锁状态,更多的哲学家,更多的筷子,情况也类似

____

死锁

  • <font color=red><font size = 5>死锁:<font color=d>死锁是非常严重的问题,他会使线程被卡主,无法继续工作,死锁这种Bug的出现具有概率性,测试的时候啥事没有,但是一发布就出问题,即使一发布了没问题,等到大家睡着了说不定又出现Bug。

⭐️⭐️⭐️⭐️⭐️死锁的四个必要条件【缺一不可,任何一个死锁的场景都必须同时具备这四个条件,少一个都不会发生死锁。】

  • 1.锁具有互斥性 (基本特点:一个线程拿到锁以后,其他线程就阻塞等待)
  • 2.锁不可抢占(不可被剥夺)一个线程拿到锁以后,除非它自己主动选择释放锁,否则别人抢不走(锁的基本特点)
  • 3.请求和保持,一个线程拿到一把锁以后,不释放这个锁的情况下,再尝试去获取其它锁。嵌套锁【代码结构】
  • 4.循环等待。(多个线程获取多个锁的过程中,出现了循环等待,A等待B,B又等待A)你等我我等你,谁都不主动的死等【代码结构】

<font color=red><font size = 5>⚠️注意:如果是自己实现的锁,就可以实现打破互斥,打破不可剥夺这两个条件,但是对于synchronized这样的锁就不行。

从第三点出发解决死锁问题

  • <font color=b><font size = 5>我们将刚刚的死锁代码进行修改,根据第三点既然嵌套会发生死锁,那我们可以将这两个锁分开,不让它们嵌套,就可以解决死锁问题

从第四点出发解决死锁问题(简单且高效的方法)

  • <font color=re><font size = 5>第四点就是你等我,我等你,互不相让造成的死锁,要解决这个问题,就需要破除循环等待,(双向奔赴)约定好加锁顺序,让所有线程按照固定顺序来获取锁,这样即使出现第三点的嵌套也不会产生死锁现象。<font color=red><font size = 5>eg:约定必需先获取 locker1 后获取 locker2 。
  • <font color=re><font size = 5>上面的代码中约定了完成 t1 执行的逻辑以后,释放完 locker1 之后,才轮到 t2 执行

多个线程获取多把锁避免死锁的解决办法

<font color=re><font size = 5>⭐️⭐️⭐️⭐️⭐️当代码中,需要用到多个线程获取多把锁,一定要约定好加锁顺序,可以有效避免死锁。如下所示:

  • 上图中滑稽老铁就餐问题,我们约定每个滑稽必需从编号小的筷子开始获取,然后才能获取编号大的;从2号老铁开始拿筷子,每人左手拿一只,但是一号老铁非要拿1号的筷子,这就会导致,1和2老铁之间发生线程阻塞;为了解决这个问题,我们可以做以下操作:
  • 先让5号老铁用4号和5号筷子,等他吃完释放筷子以后,4号老铁就可以拿起4号和3号筷子,4号吃完释放以后,3号老铁就可以拿起3号和2号筷子吃面,吃完释放,2号就可以拿起2号和1号筷子,吃完释放后,1号老铁就能拿起1号和5号筷子去吃面条了,这就能保证每个老铁都能根据自己的需求拿到对应号码的筷子去吃面条,避免阻塞等待。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 温故而知新
  • 线程安全问题
  • 多线程中有的线程未加锁
  • 一个线程有多把锁
  • 死锁三大场景
    • 场景1: 非可重入锁 ——(多套几个锁即可解决死锁)
    • 场景2: 两个线程两把锁【你在等我,我也在等你,不主动就永远没有故事直到死亡~】—— (通过jconsole调用栈+状态定位解决)
    • 场景3:N个线程,M把锁
  • 死锁
  • ⭐️⭐️⭐️⭐️⭐️死锁的四个必要条件【缺一不可,任何一个死锁的场景都必须同时具备这四个条件,少一个都不会发生死锁。】
    • 从第三点出发解决死锁问题
    • 从第四点出发解决死锁问题(简单且高效的方法)
  • 多个线程获取多把锁避免死锁的解决办法
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档