【JavaEE精炼宝库】多线程(4)深度理解死锁、内存可见性、volatile关键字、wait、notify

目录

一、死锁

1.1 出现死锁的常见场景:

1.2 产生死锁的后果:

1.3 如何避免死锁:

二、内存可见性

2.1 由内存可见性产生的经典案例:

2.2 volatile 关键字:

2.2.1 volatile 用法:

2.2.2 volatile 不保证原子性:

2.2.3 volatile 作用总结:

三、wait 和 notify

3.1 wait 详解:

3.2 notify 和 notifyAll:

3.2.1 notify:

3.2.2 notifyAll:

3.3 面试题:wait 和 sleep 的区别:


在上一篇文章,我们了解了什么是线程安全,分析了产生线程不安全的原因。今天我们就要深度刨析一下线程不安全的经典案例:死锁和内存可见性引起的线程不安全问题。

一、死锁

1.1 出现死锁的常见场景:

• 场景一:

锁是不可重入锁(synchronized 是可重入锁),并且一个线程针对一个锁对象,连续加锁两次。

• 场景二:

两个线程两把锁。先让两个线程分别拿到一把锁,然后再去尝试获取对方的锁,这时就出现了死锁的情况。

• 场景三:

多个线程,多把锁。随着线程和锁的数目的增加,情况就会变得更加复杂,死锁就更容易出现。下面就是一个经典的死锁场景:哲学家就餐(除非吃到面条,否则不会放下筷子)。

 如果出现极端的情况,同一时刻所有的哲学家都拿起左边的筷子,这时就会出现死锁。

1.2 产生死锁的后果:

死锁是非常严重的问题。一个进程中线程的个数是有限的,死锁会使线程被卡住,没法继续工作。更加严重的是,死锁这种 bug 往往都是概率性出现(未知才是最可怕的)。测试的时候,怎么测试都没事,一旦发布,就出现了问题。更加要命的是发布也没有问题,等到夜深人静的时候,大家都睡着的时候,突然给你来点问题,直接带走年终奖😭。

1.3 如何避免死锁:

要想避免死锁,我们就要从产生死锁的原因入手。

教科书上经典的产生死锁的四个必要条件(下面给出的四个条件,友友们一定要背下来,面试的经典问题)。

1. 锁具有互斥性:

这时锁的基本特点,一个线程拿到锁之后,其他线程就得阻塞等待。

2. 锁具有不可抢占性(不可剥夺性):

一个线程拿到锁之后,除非他自己主动释放锁,否则谁也抢不走。

3. 请求和保持:

一个线程拿到一把锁之后,不释放这个锁的前提下,再尝试获取其他锁。

4. 循环等待。

多个线程获取多个锁的过程中,出现了循环等待,A 等待 B ,B 又等待 A。

在任何一个死锁的场景,都必须同时具备上述四点,只要缺少一个,都不会构成死锁。观察上面的四个条件不难发现条件 1 和条件 2 是锁的基本特性,这个我们无法改变,观察到条件 3 和条件 4 都是代码结构的问题,所以我们就从条件 3,4 入手。

• 针对条件 3:

不要让锁嵌套获取即可。如果有些场景必须要嵌套获取锁,那么就破除循环等待(条件 4 ),即使出现嵌套,也不会出现死锁。

• 针对条件 4:

当代码中,确实需要用到多个线程获取多把锁,一定要记得约定好加锁的顺序(每个线程都必须要先获取 A 锁,再获取 B 锁,再.......),就可以有效避免死锁了。

二、内存可见性

2.1 由内存可见性产生的经典案例:

请友友们观察一下下面这段代码,可以粘贴到自己的编译器上跑一下,看看是否符合你的预期。

public class demo1 {
    static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(count == 0){

            }
            System.out.println("t1.end");
        });
        Thread t2 = new Thread(() -> {
            Scanner in = new Scanner(System.in);
            System.out.println("请输入一个数字:");
            count = in.nextInt();
        });
        t1.start();
        t2.start();
    }
}

因为输入数据存在 IO 操作(很慢)所以一定能保证在我们输入数据的时候,t1 线程已经开始执行了。

正常来说,我们输入一个非 0 的数字后,t1 线程里面就会停止循环。但是产生的结果如下:

循环并没有退出,由于是前台线程,所以程序不能够结束。

上述问题产生的原因就是因为内存可见性

• 案例解析:

上面的案例产生的问题是由于编译器优化 / JVM 优化产生的问题。不是说优化不好,而是 JVM 在这种情况下的优化太激进了。为什么会产生这么激进的优化呢?

我们站在指令的角度来理解有两个方面:

1. 在while 循环体中,每次条件判断的时候,分为两个步骤:1. load:从内存读取数据到 cpu 寄存器。2. cmp:比较

评论 129
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值