乐趣区

关于java:蹲坑也能进大厂多线程系列上下文死锁高频面试题

作者:花 Gie

微信公众号:Java 开发零到壹

前言

上一期的蹲坑系列咱们介绍了多线程的基础知识,是不是和你平时的理解有些出入呢。

《蹲坑也能进大厂》多线程这几道根底面试题,80% 小伙伴第一题就答错

明天持续解说多线程绝对根底的实践知识点,如果你是老手或者对多线程理解不多,千万不要想着下来就肝实战课,没用的,轻易出一个 bug 你都看不出来啥起因,花 GIe 强烈建议跟着本系列走完(手动狗头护体)。

我:哟呼,狗剩子明天怎么不在家涵养,也不陪你女朋友,又来公司写 bug 啊

狗剩子:嘿嘿,怎么可能不陪女朋友,咱们每天如影随行

我:……

狗剩子:趁着明天没人,走,坑里见,我要和你坦诚绝对。

注释

我:狗剩子,请听第一题,守护线程和用户线程有什么区别?

咱们应该晓得,Java 有两种线程:【守护线程 Daemon】与【用户线程 User】,两者的惟一的区别就是:虚拟机来到时,如果 JVM 中所有线程都是守护线程时,JVM 就会主动退出;然而如果还有一个或以上的非守护线程则不会退出。

我:昨天问过你 notify,那你晓得 Java 中 notify 和 notifyAll 有什么区别?或者说咱们怎么抉择应用哪一种?

昨天的事竟然还记得,你这忘性还能够呀,我都遗记了。

是这样的,notify不能指定唤醒某一个具体的线程(这是网上说的,俺就跟着说,至于为啥前面通知你),可能会导致信号失落这样的问题,只有在一个线程期待的时候才是它的主场,而 notifyAll 会唤醒所有期待线程,并容许他们抢夺锁,尽管效率不高,然而能够保障至多有一个线程继续执行。如果想要应用notify,必须确保满足以下两种状况。

  • 一次告诉仅须要唤醒最多一条线程。
  • 所有期待唤醒的线程,本身解决逻辑雷同。举个栗子大家就会明确,比方应用 Runnable 接口实例创立的不同线程,或者同一个 Thread 子类 new 进去的多个实例。

我:不要得意,这都是开胃菜,再问你一个,wait 为什么只能在代码块中应用?

啥?(心里捣鼓)wait只能在代码块中应用吗,我咋不晓得。那是 …. 可能 wait 有洁癖,喜爱一个人自嗨吧。

我:你踏马 ….

哦 … 我想起来了,咱们能够反过来想,如果 wait 不要求在同步块中,那可能会产生以下的谬误。

先看一处用 wait、notify 实现的线程平安队列的代码:

class BlockingQueue {Queue<String> buffer = new LinkedList<String>();
 
    //
    public void give(String data) {buffer.add(data);
        notify();                   // Since someone may be waiting in take!}
 
    public String take() throws InterruptedException {while (buffer.isEmpty())    // don't use"if" due to spurious wakeups.
            wait();
        return buffer.remove();}
}
  1. 消费者 A 调用 take(),此时 buffer.isEmpty() 为 true;
  2. 消费者 A 进入 while,在调用 wait() 办法之前, 生产者 B 调用了一个残缺的 give()(即 buffer.add(data) 和 notify());
  3. 之后消费者 A 调用了 wait(),然而错过了生产者 B 调用的notify()
  4. 如果之后没有别的生产者调用 give()办法,消费者 A 所在线程则会始终期待。

我:这波解释我都忍不住给你点个赞,你晓得 Java 中锁是什么吗?

锁的概念小伙伴们也能够,在学习完花 Gie 的 volatile 之后再看。

这个货色说起来很形象, 你能够就把它设想成事实中的锁, 至于他的防盗锁、金锁、还是指纹锁并不重要,哪怕它就是一根草绳,一个自行车、甚至一坨那啥,都能够当做锁。 是什么外在形象并不重要,重要的是它代表的含意: 谁持有它, 谁就有独立拜访临界资源的权力。

我:你有理解过什么是线程死锁吗?

死锁 就是说两个或两个以上线程在执行过程中,因为竞争资源,它们都在期待某个资源被开释,因而线程被无限期地阻塞,此时的零碎处于死锁状态。

如图,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会相互期待而进入死锁状态。

我:那你晓得造成死锁的四个必要条件吗?

咱家有啥不晓得的,这就给你列举进去。

  1. 互斥条件:线程(过程)对调配到的资源具备排他性,也就是说该资源任意一个时刻只能有一个过程占用。
  2. 申请与放弃条件:一个线程(过程)因申请资源而阻塞时,对已取得的资源放弃不放。
  3. 不剥夺条件:线程(过程)已取得的资源在末应用完之前不能被其余线程强行剥夺,只有本人应用结束后才开释资源。
  4. 循环期待条件 :当产生死锁时,所期待的线程(过程) 必然造成一个环路,死循环造成永恒梗塞。

我:既然你晓得造成死锁的条件,那你必定晓得如何防止咯?

正所谓隔靴搔痒,想要 防止死锁,那就须要破坏者四个必要条件的任意一个即可。

  1. 毁坏互斥条件:此路不通,因为咱们用锁原本就是想让他们互斥的。
  2. 毁坏申请与放弃条件:一次性申请所有的资源。
  3. 毁坏不剥夺条件:占有资源线程能够尝试申请其它资源,如果申请不到,能够被动开释它占有的资源。
  4. 毁坏循环期待条件:依照某一程序申请资源,开释资源时则反序开释。

我:这波答复的不错,昨晚是不是偷偷筹备了。狗剩子请持续听题,上下文切换知道吗?

麻烦尊重俺一下,当前请叫狗爷。

说到 上下文切换 ,那咱们得先晓得什么是 上下文 ,直白说 上下文 就是某个工夫点 CPU 寄存器和程序计数器的内容。

拓展:每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保留以后线程的工作变量),堆栈(记录执行历史,其中每一帧保留了一个曾经调用但未返回的过程)。

寄存器:寄存器就是 CPU 外部内存,负责存储曾经、正在和将要执行的工作,数量较少然而速度很快,与之绝对应的是 CPU 内部绝对较慢的 RAM 主内存。

程序计数器:程序计数器是一个专用的寄存器,用于表明指令序列 CPU 以后执行的地位,存储的内容是正在执行指令的地位或下一次将要执行指令的地位。

大抵理解了 上下文 ,那 上下文切换 也就简略了,它是指当前任务执行完,CPU 工夫片切换到下一个工作之前会先保留本人的状态,以便下次再切换回这个工作时能够继续执行上来,工作从保留到再加载执行就是一次上下文切换。

如果还不是非常分明,能够分上面三个步骤了解。

  1. 挂起一个过程,将这个过程在 CPU 中的状态(上下文)存储在内存中;
  2. 在内存中检索下一个过程的上下文,并且将该过程在 CPU 的寄存器中回复;
  3. 跳转到程序计数器所指向的地位,也就是该过程被中断时,代码过后执行到的地位,从而复原该过程。

我:讲的好具体,忽然好心动,那上下文切换会带来什么问题呢?

看完下面介绍咱们应该有一个感觉,那就是如果高并发状况下,频繁切换上下文会导致系统串行执行,运行速率大大降低。

  • 间接耗费:包含 CPU 寄存器须要保留和加载, 系统调度器的代码须要执行。
  • 间接耗费:CPU 为了放慢执行速度,会把罕用的数据缓存起来,然而当上下文切换后(即 CPU 执行不同线程的不同代码),那本来所缓存的内容很大水平没有利用价值了,因而 CPU 就会从新进行缓存,这也导致线程被调度运行后,一开始启动速度会比较慢。

拓展:线程调度器为了防止频繁切换上下文带来的开销,会给每个被调度到的线程设置一个最小执行工夫,从而缩小上下文切换的次数,从而进步性能,然而毛病也不言而喻,就是会升高响应速度。

我:那你跟我讲一下 volatile 是啥呗?

不了不了,明天快累死咱家了,老衲须要劳动劳动,今天咱们再战。

总结

多线程知识点十分宏大,波及到很多方面,特地是刚刚接触多线程的小伙伴,对于 上下文 这种概念了解起来十分艰难,想要真正全副把握须要深究每一个问题所波及到的知识面,比方 怎么用 wait/notify 实现生产者消费者模式 线程的调度过程 Java 代码如何一步步转化被 CPU 执行,还有十分重要的,就是下面这些知识点的原理是什么,Thread 如何启动、中断线程的 线程间进行通信的原理是什么

这些花 GieGie 前面都会逐渐带大家把握,有些大的知识点会拿进去进行单篇的解说,心愿大家继续关注,假日不打样,持续肝

点关注,防走丢

以上就是本期全部内容,如有纰漏之处,请留言指教,非常感谢。我是花 GieGie,有问题大家随时留言探讨,咱们下期见🦮。

文章继续更新,能够微信搜一搜「Java 开发零到壹」第一工夫浏览,后续会继续更新 Java 面试和各类知识点,有趣味的小伙伴欢送关注,一起学习,一起哈🐮🥃。

原创不易,你怎忍心白嫖,如果你感觉这篇文章对你有点用的话,感激老铁为本文 点个赞、评论或转发一下,因为这将是我输入更多优质文章的能源,感激!

退出移动版