JavaEE 初阶(7)——多线程5 之线程安全中 -->“死锁”

目录

一. 什么是“死锁”

二. 产生死锁的场景 

 场景1:一个线程连续加锁

 场景2:两个线程两把锁

场景3:N个线程M把锁 

三. 产生死锁的四个必要条件(缺一不可)

四. Java 标准库中的线程安全类


一. 什么是“死锁”

并非是 synchronized 就一定线程安全,还要看代码具体咋写。到底是否加 synchronized ,和具体场景直接相关。“无脑加锁”的做法不推荐,锁需要的时候才使用,不需要的时候不要使用,会付出代价(性能)。使用锁,就可能触发阻塞,一旦某个线程阻塞,啥时候能恢复阻塞,继续执行,是不可预期了...(可能需要非常多时间)

因此,synchronized 如果使用不当,就会出现“死锁”

死锁:发生在多个进程或线程在执行过程中,因为竞争资源而造成的一种互相等待的僵持状态,没有任何一个进程或线程可以继续执行下去。简单来说,死锁是指两个或多个进程无限期地等待永远不会发生的条件,导致它们无法继续执行。

二. 产生死锁的场景 

 场景1:一个线程连续加锁
public class DeadLock2 {
    private static int count = 0;

    public void add(){
        synchronized (this){
            synchronized (this){
                count++;
            }
        }
    }
  
}

运行结果: 

代码分析:

  1. 里面的synchronized要想拿到锁,就需要外面的synchronized释放锁
  2. 外面的synchronized要释放锁,就需要执行到 }
  3. 要想执行到 } 就需要执行完 count++
  4. 但是 count++ 阻塞着呢~

这样,就会一直阻塞等待,造成“死锁”.....

但是,实际运行并没有出现“死锁”现象,这是为什么呢?

因为 synchronized 针对这种情况做了特殊处理~

synchronized 是 “可重入锁”,针对上述一个线程对同一把锁连续加锁做了特殊处理,是Java为了减少程序员写出死锁的概率,引入的特殊机制。同样的代码,换成C++ / Python 就会出现“死锁”。

可重入锁” 的一个主要特性是它允许同一个线程多次获取同一把锁。如果线程已经持有了这把锁,那么它可以再次进入由这把锁保护的代码块,而不会阻塞产生“死锁”。这是通过记录锁的持有者,并引入一个计数器来实现的:

初始情况下,计数器是0  --> 执行到 { 计数器 +1 ,执行到 } 计数器 -1。如果某次 -1 之后,计数器为0了,说明这次就要真正释放锁了。

此处涉及到了 “引入计数” 的思想,后面讲到 JVM 中的垃圾回收机制也会有 

 场景2:两个线程两把锁
public class DeadLock {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("t1 加锁locker1 完成");

                //这里的sleep是为了让t1和t2都先拿到自己锁,然后再拿对方的锁
                //如果没有sleep执行顺序就不可控
                //可能出现某个线程一口气拿到两把锁,另一个线程还没执行,无法构造出死锁.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (locker2){
                    System.out.println("t1 加锁locker2 完成");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized(locker2){
                System.out.println("t2 加锁locker2 完成");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized(locker1){
                    System.out.println("t2 加锁locker1 完成");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

运行结果:

 

“死锁”场景描述:

  1. t1 线程先对 locker1 加锁,t2 线程先对 locker2 加锁   
  2. t1 线程在不释放 locker1 的情况下,对 locker2 加锁;同时,t2 线程在不释放 locker2 的情况下,对 locker1 加锁

这样,就会造成“循环依赖”的效果,产生“死锁”。

形象的比喻:疫情期间,一码通又寄了.......程序员来到公司楼下,被保安拦住了。
保安:请出示一码通
程序员:我得上楼修了bug,才能出示一码通
保安:你得出示一码通,才能上楼

通过上述例子,“死锁”往往会出现“依赖循环”。针对这种“死锁”情况,“可重入锁”机制就无能为力了....


 

通过观察 jconsole,我们发现此时 t1 和 t2 线程都处于 BLOCKED 阻塞状态

解决办法:t1 线程可以先释放 locker1,再请求 locker2

场景3:N个线程M把锁 

经典模型:哲学家就餐问题

 现在共有 5个 哲学家,桌子上有 5根筷子。5个哲学家要吃桌子上的面条。 

 此时,每根筷子都被哲学家左手拿起来了,他们的右手都拿不到筷子了.....由于哲学家 非常固执,当他吃不到面条的时候,也绝对不会放下左手的筷子.....这样,每一个人都吃不到面条了,只能循环等待....


“哲学家”相当于“线程”,“筷子”相当于“锁”。如果线程1拿A锁,线程2拿B锁,线程3拿C锁,线程4拿D锁,线程5拿E锁。这时候,5个线程再同时请求等待旁边的锁,就会造成“死锁”的局面....

但是,上述情况是由代码结构造成的,可以通过一些方法来避免。


 比如, 必须先针对编号小的锁加锁,后针对编号大的锁加锁。每个哲学家必须先拿起编号小的筷子 ,后拿起编号大的筷子。同一时刻,拿起第一根筷子

在编写代码时,可以给锁编号:1,2,3.....N。规定所有线程在加锁的时候,都必须按照一定的顺序来加锁(比如,必须先针对编号小的锁加锁,后针对编号大的锁加锁)

 

三. 产生死锁的四个必要条件(缺一不可)

a. 互斥条件(锁的基本特性):同一把锁 锁住的代码块 一次只能由一个线程执行(基本特性无能为力)

b. 不可被抢占条件(锁的基本特性):线程1 拿到了锁A,如果线程1 不主动释放锁A,线程2就不能把锁A抢过来(基本特性无能为力)

c. 持有和等待条件(代码结构):线程1 在持有 A锁 的情况下(持有),去拿B锁(等待);线程2 持有 B锁 的情况下(持有),又去拿A锁(等待)。(解决办法:线程1 可以先释放A锁,再请求B锁)

d. 循环等待 / 循环依赖 / 环路等待 条件(代码结构):每个线程至少持有一个把锁,并等待获取下一个线程所持有的锁,构成一个循环等待链。(解决办法:给锁编号,并约定要按照一定的顺序加锁)

四. Java 标准库中的线程安全类

 Java标准库中很多都是线程不安全的。这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施,因此,可以避免很多隐形加锁情况,防止产生“死锁”

ArrayList,Queue,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder......

但是还有一些是线程安全的,使用了一些锁机制来控制,内置了synchronized,这些类是不推荐使用的,甚至 jdk 未来版本,会把这几个东西删掉....

Vector,HashTable,Stack,StringBuffer....

例如:StringBuffer中  

  • 10
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值