Java 从多线程到并发编程(五)—— 线程调度 优先级倒置(反转) 阻塞 死锁 suspend

文章目录

前言 ´・ᴗ・`

这一节比较轻松 是对前面几节的一个小小的补充,主要涉及对线程优先级相关的拓展,另外,我们这次稍微深度剖析各种各样的阻塞情况,这对我们后边理解各种机制如wait notify,以及解决生产者消费者问题等都是非常关键的铺垫。

阻塞讲完,那死锁概念也将呼之欲出。最后,我们会对之前没讲的suspend为什么deprecated做个说明。

线程调度策略

对于单核cpu,在任意时刻只能执行一条机器指令,一个线程 而CPU的高频切换,实现了宏观并行,微观串行的效果

前面我们说 在运行池中,会有多个处于就绪状态的线程在等cpu, JAVA虚拟机有专门的线程调度器来分配CPU的执行计算的权利(类似老丈人挑线程女婿 CPU是女神 )

下面是主流的两种线程调度方式:

抢占式调度
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制

在这种机制下,一个线程的堵塞不会导致整个进程堵塞。

这种就需要线程调度器来给就绪的线程们(上门求婚的男士)排排序 然后确定真正的优先级

协同式调度
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行。
这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

而进程也有相应的调度算法 根据不同的系统 多种应用场景 其算法非常丰富 有兴趣可以查别的大佬的文章

优先级倒置问题

我们上一节提到了优先级的问题,而且也说了优先级的高低并不能决定这个线程真的能够执行或者只是**就绪(Runnable)**而拿不到CPU,我们说,这与线程调度器的策略(上面刚刚说完)很有关系,其实这容易让人误解,好像线程调度器有自己的想法?不按照我们设定好的优先级来执行?

其实就算它“很想”按我们设定好的优先级工作,也往往未能如愿。这里我们聊聊,什么样的巧合会导致,“未能如愿”,即 优先级倒置问题,意思很简单,优先级高的反而得不到CPU,优先度低的得到了CPU,地位发生了倒置。

Why?正常来说,无论怎么样,高优先级的任务一来,低优先级任务就会让位,所以似乎必定高优先级任务能拿到CPU,那为啥还会有优先级倒置?

让我们考虑这么个例子,优先级 T1>T2>T3

  • T3先执行,他执行的时候正好拿到了资源A,意味着别的任务不能访问资源A,而会被阻塞
  • 这时 T2执行,由于优先级更高,他很轻松的抢占了CPU 然后他成功的访问到了资源B,成为唯一能够访问资源B,拿到锁的幸运儿。有趣的是,资源B也是T3所需要的,但是这不影响,因为按理来说,T2会执行,直到释放B的锁,以及让出CPU,这样T3可以继续执行
  • 这时出现了T1,他毫不意外的从T2抢占了CPU,但是却没办法执行,因为缺乏资源A!所以理论上他需要T3执行完才能继续执行。。。TT

你说接下来发生什么?自然是T1被阻塞,然后让出CPU,实际上最先执行的是T2,等T2执行完,T3才能获得CPU(此时因为T1被阻塞了,没办法抢占CPU),继续执行,等到T3执行完,释放T1所需的资源A,最后才轮到T1.

优先级倒置解决方案

目前据我所知有两种解决方案

优先级继承
我们发现问题出在,一个矛盾,高优先级的任务因为低优先级任务持有的资源(当然不是CPU资源),而阻塞,既然我们没法轻易释放锁,解锁,因为锁需要CPU运行才能释放,那我们干脆直接让,那个获得资源的任务,得到相同的优先级,或者说继承那个高优先级,这样原先的低优先级任务,可以先运行,赶紧用完赶紧走,别打扰我T1干事

换个角度来看,这就是临时提升CPU的抢占优先级,临时“升官”,为的就是赶紧执行让出资源。

资源让出来以后,再恢复优先级,该干啥干啥:)

这样 一开始你会觉得 这不还是让T3先于T1了嘛?但是你别忘了,我们这层优化可以使得T1不用在T2之后执行

So?不就节省了一个任务嘛?那如果我改变一下条件,假设有100个任务 T1 ~ T100,T100最低,持有T1的资源,你觉得会发生什么事?

很明显T1会*等待T2~T99,所有的任务都执行完才能执行!!!!

这就非常显著的性能差异了

优先级天花板
除了优先级继承模式,还有个模式被称为优先级天花板,
这个模式很好玩的在于,他直接避免了阻塞的发生:)优先级继承的机制触发条件是,高优先级任务被手握资源的低优先级任务阻塞,而天花板则是,只要他手握资源,那么在所有可能需要此资源的线程中,他拥有最高优先级,

这意味着,其他资源的占有比CPU的占有更重要,占有其他临界资源,我可以给你配好CPU的使用优先级,但你如果只是占有CPU 实际上你还被阻塞 等同于没有优先

死锁 dead lock

互相抱着对方想要的资源 然后互相想要对方手上的资源 谁都不能执行下去 最终全部被互相阻塞,寸步难行

suspend

其实死锁还可以有一点变式,我们知道,我拿着你所需要的资源,最终导致的是什么?是你不能执行是吧,你被阻塞

suspend这个方法就能做到这点,他可以阻塞一个线程。这听起来似乎没啥问题啊

但是为啥他变成这幅模样:
Java 从多线程到并发编程(五)—— 线程调度 优先级倒置(反转) 阻塞 死锁 suspend
我们知道,即便一个线程被阻塞,他还是拥有之前一些资源的锁,听起来很奇怪,但是如果不这样,他可能就会“捡了西瓜丢西瓜”,为了捡一个锁,必须释放之前的锁,而收集所有的锁才能完成一项任务,这导致他永远完成不了任务:)因为锁捡一个丢一个。所以,他持有着之前的锁,这就衍生出一个问题:

假设,一个线程A持有锁A1,之后他被外部的我们给挂起suspend了,进入阻塞状态,后边,只有一个线程B能够解除其阻塞状态(唤醒),但是问题在于,B需要资源来唤醒A,恰恰是A持有的锁A1所对应的临界资源,那这就很有意思了:)

A永远不可能被唤醒,因为他拿着A1,而B要唤醒A只能通过A1这个资源,A却给不了B(没办法运行在CPU 没法释放资源)

换言之,也可以说A与B形成死锁 因为A用临界资源A1卡住了B,让B没法运行,阻塞,而能够解除A阻塞的只有B,互相卡脖子

被阻塞的同时持有资源不放 是上述问题的诱因

我们前面一直在说的问题,比如优先级倒置,比如死锁(包括suspend可能导致的后果),其实似乎都有一个小的诱因来决定,那就是因为某个还占用着资源A的线程,在被阻塞之后,还占用着资源,

这导致另一个需要资源A的线程也被阻塞,因为资源只供一个线程访问,其他访问的线程都只能干瞪眼,只能阻塞。这类似占着茅坑不拉屎,尸位素餐的意思,应当让能者上任,知人善任才对。

你看死锁,不就是两个线程,互相抱着对方需要的资源,导致最后互相阻塞,谁都执行不了。

而优先度倒置,根据理想中我们想的调度算法设计,高优先级进程就是可以抢占低优先级的CPU资源,CPU先执行高优先级任务,但是不幸的是,线程的执行除了CPU,还需要别的资源,那假设,你需要的资源刚好在,你刚刚抢占的线程手中呢?

你确实获取了CPU,但是寸步难行,于是只能阻塞了,主动让出CPU,这样低优先级的任务才能执行,而你只能等低优先级(往往比较耗时)的任务做完,眼巴巴看它释放资源,你才有可能进去,然后继续运行。

总结 ´◡`

这一节我们又解决了一些因为阻塞导致的几个问题,后边两节我们将进入新的高度,为了解决生产者消费者问题而学习,

下一节 Java 从多线程到并发编程(六)—— 并发 同步 锁 阻塞 synchronized四种应用形式

上一篇:Kotlin语言学习笔记(8)协程


下一篇:idae 对Flink 集群进行远程调试