【多线程】多线程(5):死锁,内存可见性

【死锁】

死锁是一个使用锁的注意事项

【出现死锁的场景】(重点掌握)

【场景一:一个线程一把锁】

这个线程针对这把锁,连续加锁两次

synchronized (this)
{
    synchronized (this)
    {
                       
    }
}

第一次加锁,加锁成功,第二次加锁,由于锁对象被用了,因此陷入阻塞,需要等锁释放才能结束阻塞,但外面的锁能够释放的前提条件是代码可以正常往下走,但里面的锁产生阻塞导致代码无法往下走,逻辑上构成了死循环,导致线程卡死无法继续进行,于是形成了「死锁」

【可重入锁】

Java中,针对于“对同一把锁,连续加锁两次”的情况作了特殊处理,这种特殊处理让synchronized变成了「可重入锁」:

额外记录一下当前是哪个线程对这把锁加锁

在加锁时,判定该锁是否被占用,如果被其他线程占用了,就并不会进行加锁操作,也不会进行阻塞操作,而是一路放行,往下执行代码

此外,可重入锁还会通过一个引用计数判断当前加锁了几次,以及在什么时候才会真正的释放锁

【场景二:两个线程两把锁】

线程1,线程2,锁A,锁B

1.线程1对A加锁,线程2对B加锁

2.线程1在不释放锁A的情况下,对B加锁,同时线程2在不释放锁B的情况下,对A加锁

这种情况下也会出现死锁

举一个例子:

A和B下馆子吃饺子,他们都有吃饺子同时蘸酱油和醋的习惯,A抄起酱油,B抄起醋

A:你把醋给我,我用完后给你

B:你把酱油给我,我用完后给你

A和B互不相让,形成僵持

这就是死循环,构成「死锁」

如何避免?先放后拿

若先释放锁A再拿锁B,则不会死锁

【场景三:N个线程M把锁】

这里可以举一个典型的哲学家就餐问题的例子

任何一个哲学家想要吃到面条,都需要拿起左手和右手的筷子,此时,每根筷子都被哲学家左手拿起来了,他们的右手都拿不起筷子

由于哲学家非常固执,即便他们吃不到面条,也绝对不会放下手中的筷子

从而构成死锁

要想避免出现这种情况,可以给锁编号,按相同的顺序加锁,约定所有的线程在加锁时,都必须按照一定的顺序进行加锁(比如先针对编号小的锁加锁,后针对编号大的锁加锁)

此时,回到哲学家就餐问题上

给所有的筷子都标了序号,5号哲学家可以拿起5号和4号筷子就餐,就餐完毕后放下筷子,4号哲学家可以拿起4号和3号筷子就餐,就餐完毕后放下筷子……直到最后一个1号哲学家就餐完毕

Thread t1 = new Thread(() ->{
            synchronized (locker1){//t1先对locker1加锁
                System.out.println("t1加锁locker1完成");
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker2){//t1后对locker2加锁
                System.out.println("t1加锁locker2完成");
            }
        });
        Thread t2 = new Thread(() ->{
            synchronized (locker2){//t2先对locker2加锁
                System.out.println("t2加锁locker1完成");
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker1){//t2后对locker1加锁
                System.out.println("t2加锁locker2完成");
            }

我们可以看到,t1是先针对locker1加锁,后针对locker2加锁,而t2不是,此时只需要让两个线程加锁顺序相同就可以避免死锁了

【出现死锁的四个必要条件】

1.锁是互斥的「锁的基本特性,不可干预」

2.锁是不可能被抢占的(线程1拿到了锁A,此时线程2也想拿锁A,若线程1不主动释放锁A,那么线程2无法把锁A抢过来,形成僵持,构成死锁)「锁的基本特性,不可干预」

3.请求和保持(线程1拿到锁A后,在不释放锁A的前提下,去拿锁B,构成死锁)「代码结构,可干预」

4.循环等待(多个线程获取锁的过程中,存在循环等待,一旦出现,会构成死锁)「代码结构,可干预」

★避免请求和保持:先放后拿

★避免循环等待:按照相同顺序进行加锁

【内存可见性】

内存可见性,是引发线程安全问题的原因之一,本质上是编译器/JVM对多线程代码进行优化时,优化出bug

public class Demo3 {
    public static int n = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
            while(n == 0){

            }
            System.out.println("t1线程结束循环");
        });

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

t1和t2线程同时运行,t2线程中用户输入一个非0值时,t1线程就会结束,反之t1线程则会一直运行下去

但真正这么做却失败了,输入了一个非0值结果t1还是没有结束

这段代码的执行机理是:

1.从内存中读取数据到寄存器中

2.通过类似于cmp指令,比较寄存器和0的值

此时JVM执行代码时,发现每次执行1操作开销很大,而且执行结果一样,JVM没有意识到用户可能在未来修改n,于是JVM直接把1操作优化掉了——每次循环,不会重新读取内存中的数据,而是直接读取寄存器/cache中的数据(缓存的结果)/当JVM作出决定后,此时意味着循环开销大幅降低,但当用户修改n值时,内存中的n发生改变,但由于t1线程每次循环不会真的读内存,因此感知不到n的改变

内存中的n的改变,对于线程t1来说,是“不可见的”,即「内存可见性问题」

【解决方法:加sleep】

Thread t1 = new Thread(() ->{
            while(n == 0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1线程结束循环");
        });

和读内存相比,sleep开销更大,远远超过读内存,就算把读内存优化掉也无意义,杯水车薪,因此不会优化读内存

【解决方法:加volatile】

多线程中编译器进行优化可能导致线程安全问题,而编译器进行优化的前提,是编译器认为:针对这个变量的频繁读取,结果都是固定的,因此可以引用“volatile”

public static volatile int n = 0;

用于修饰一个变量的关键字,提示编译器这个变量是“易变的”

引入volatile后,编译器在生成代码时,会给变量的读取操作附近生成一些特殊的指令,称为「内存屏障」,后续JVM执行到这些特殊指令时,就知道了不能进行上述优化

//volatile只是解决内存可见性问题,不能解决原子性问题,如果两个线程针对同一个变量进行修改,那么volatile就无能为力了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值