java基础巩固-宇宙第一AiYWM:为了维持生计,多高(多线程与高并发)_Part1~整起(线程与进程篇:线程概念、线程状态、线程死锁)

这个题目我感觉很多大哥大姐和我一样,虽然夹在众位大哥大姐中跟着一块喊着“多线程与高并发”的口号,但是这里面其实包含的东西并不像名字里面这么少。现在就开始咱们的旅程吧。

特此感谢,低编程并发(微信公众号这位老师),以及B站的狂神说老师,课和文章都挺好,大家可以去看看。还有其他大牛们,本人的笔记离不开各位老师的引导。
开唠

1.首先,咱们为啥要用多线程呢?

单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率+多核时代主要是为了提高 CPU 利用率【多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销,具体见下:
在这里插入图片描述

  • 一个 Java 程序的运行是 main 线程和多个其他线程同时运行
    • 多核 CPU 时代意味着多个线程可以同时运行,也就是并行同一时刻多个线程可以使用自己的CPU一块执行而并发【并发:线程们轮流使用CPU的做法称作并发,concurrent】是多个线程有一定的时间间隔的在执行,只是切换的很快肉眼看不到而已,相当于是OS中的任务调度器将CPU的时间片分给不同的线程轮流切换使用,微观上这些线程们是串行执行的,只是咱们人肉眼看不到间隔,宏观上感觉是并行的),这减少了线程上下文切换的开销
      在这里插入图片描述
    • 多线程编程中线程个数一般都大于CPU个数(线程一般很多嘛),而 每个CPU同一时刻只能被一个线程使用(为了能让咱们用户感觉多个线程是在同时执行的,提高用户体验嘛),CPU资源的分配采用了时间片轮转的策略(也就是给每个线程分配一个时间片,每个线程只在自己对应的时间片内占用CPU并执行任务,当前线程使用完自己对应的CPU时间片后当前这个线程自己就会处于就绪状态并让出CPU给其他线程,这也就是线程的上下文切换)。【线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。】
      • 那么就有个问题,让出 CPU 的线程等下次轮到自己占有 CPU 时如何知道自己之前运行到哪里了?—看这里线程如何知道切出去前自己运行到哪里
      • 线程的上下文切换时机:【线程在执行过程中会有自己的运行条件和状态(也称上下文),比如程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。】
        • 当前线程的CPU时间片使用完,当前线程处于就绪状态【时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。】
        • 被终止或结束运行
        • 当前线程被其他线程中断时
          • 主动让出 CPU,比如调用了 sleep(), wait() 等。
    • 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。多线程机制可以大大提高系统整体的并发能力以及性能,多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
  • 线程相比进程能减少开销 体现在:
    • 多线程模型:
      在这里插入图片描述
    • 线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存管理信息、文件管理信息,而线程在创建的过程中,不会涉及这些资源管理信息,而是共享它们
    • 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
    • 同一个进程内的线程切换比进程切换快【线程间的切换和调度的成本远远小于进程】,因为线程具有相同的地址空间(虚拟内存共享),这意味着 同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于 进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的
      • 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息;】
    • 同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了
  • 提前说好,并发不一定要依赖多线程,像PHP中很常见的 多进程并发
    • 多进程模型
      在这里插入图片描述
    • 单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发。宏观上好像是CPU同时在运行多个进程,但是其实实际上还是一个又一个进程以一定的时间间隔在运行。

2.三种线程的实现方式

  • 线程(其实,每个已经执行了start()而且还没有结束的java.lang.Thread类的实例就代表一个线程可以点一点,看看这篇线程启动的几种方式)可以比作是轻量级的进程(线程是进程中的一个实体,是进程的一个执行路径线程本身不会独立存在),是程序执行的最小单位,线程间的切换和调度的成本远远小于进程(比如说,进程搞来了一个单位的资源来分配调度,分配调度好了之后,线程兜里揣着这些资源去执行任务线程的(运行时数据区域)内存区域分布
    在这里插入图片描述
    • 主要有三种线程的实现方式:
      • 用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
        • 用户线程是基于用户态的线程管理库来实现的,那么线程控制块(Thread Control Block, TCB) 也是在库里面来实现的,对于操作系统而言是看不到这个 TCB 的,它只能看到整个进程的 PCB。所以,用户线程的整个线程管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
          在这里插入图片描述
        • 用户线程的优缺点:
          在这里插入图片描述
      • 内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程
        • 内核线程是由操作系统管理的,线程对应的 TCB 自然是放在操作系统里的,这样线程的创建、终止和管理都是由操作系统负责
          在这里插入图片描述
        • 内核级线程的优缺点:
          在这里插入图片描述
      • 轻量级进程(LightWeight Process):在内核中来支持用户线程
        • 轻量级进程(Light-weight process,LWP)是内核支持的用户线程,一个进程可有一个或多个 LWP,每个 LWP 是跟内核线程一对一映射的,也就是 LWP 都是由一个内核线程支持。另外,LWP 只能由内核管理并像普通进程一样被调度。LWP与普通进程的区别也在于它只有一个最小的执行上下文和调度程序所需的统计信息。一般来说,一个进程代表程序的一个实例,而 LWP 代表程序的执行线程,因为一个执行线程不像进程那样需要那么多状态信息,所以 LWP 也不带有这样的信息。
    • 线程的运行空间来说,线程可以分为:
      • 用户级线程(user-level thread, ULT)
        • 它仅存在于用户级中,这种线程是不依赖于操作系统核心的。应用进程利用线程库来完成其创建和管理,速度比较快,操作系统内核无法感知用户级线程的存在。
      • 内核级线程(kernel-level, KLT)
        • 类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程它们的创建、撤销和切换都由内核实现。比如英特尔i5-8250U是4核8线程,这里的线程就是内核级线程

3.那为什么要用高并发呢:

  • 充分利用计算机处理器的能力,你想想CPU多快呀,磁盘、网络通信或者数据库访问等相比之下就慢多了,所以你肯定不想把时间都浪费在等待上面吧
  • 现在的时代,对应用系统性能以及吞吐量要求越来越高,出现了处理海量数据和请求的要求,所以就催生了对高并发编程的迫切的要求
    • 现在HTTP下一个服务端同时对多个客户端提供服务这种并发应用场景很常见。咱们很多人同时上XX买东西,更有甚者进同一家店买同一个东西同一时间下单等,此时对于计算量相同的任务程序线程并发协调的越有条不紊效率自然越高。换句话说你要是线程之间经常切换,用户态内核态轮流干,频繁阻塞甚至死锁,这不卡死。
      在这里插入图片描述

4.打手的故事

然后,得慢慢请出咱们多高这部戏的主角,先是“打手”(打手来源见JVM_Part3中扫垃圾时,打手咋跑出来了)
咱们OS在分配资源时:由于线程的引入就可以把一个进程的资源调度分配和执行调度分开:

  • 把(除CPU之外的资源)内存等资源分配给进程,比如内存地址、文件IO等
    • CPU中有PC、寄存器们等,关于CPU的结构以及具体的工作方式大家可以网上看看,知道CPU是干啥的玩意就行
  • OS把CPU分配给线程

提到这两个重要的点,可能大家和我一样会有疑问,为啥要这样分呢。(我之前关于JVM的部分中也有提到,大家可以去翻翻)我感觉就一句话,一个进程中生活着很多个线程,线程们是真正占用CPU资源去运行或者说执行进程中的各个任务呢。比如说 咱们打开了一个应用程序比如QQ,相当于咱们现在开启了一个进程,那么QQ里面咱们是不是存在很多小的任务比如聊天呀听音乐呀种个菜呀发个邮件呀等等(当然可能不是这么分的,大家理解意思就好),整个QQ应用程序中的多个小任务都得生活在进程中的多个线程来执行。而OS中一般是CPU来执行任务的。所以,精准定位,把CPU分配给线程不就刚好了,那么这么多线程不就可以带着自己趁手的兵器(方天画戟)就打怪闯关了。此时,咱们的打手也找到自己的称手的兵器的,可以开打了。

  • 一个进程中生活着很多个线程,线程们是真正占用CPU资源去运行或者说执行进程中的各个任务呢。而CPU一般是使用时间片轮转的方式让进程中的多个线程们轮询占用的(当前线程CPU时间片用完后要让出CPU,等下次轮到自己时再执行)
  • 在 Linux 内核中,进程和线程都是用 task_struct 结构体表示的,区别在于:
    • 线程的 task_struct 结构体里部分资源是共享了进程已创建的资源,比如内存地址空间、代码段、文件描述符等,所以 Linux 中的线程也被称为轻量级进程,因为线程的 task_struct 相比进程的 task_struct 承载的资源比较少,因此以轻得名。
      • 每一个进程都有一个数据结构 task_struct,该task_struct结构体里有一个指向文件描述符数组的成员指针该数组里列出这个进程打开的所有文件的文件描述符。数组的下标是文件描述符,是一个整数,而数组的内容是一个指针,指向内核中所有打开的文件的列表,也就是说内核可以通过文件描述符找到对应打开的文件
    • 没有创建线程的进程,是只有单个执行流,它被称为是主线程。如果想让进程处理更多的事情,可以创建多个线程分别去处理,但不管怎么样,它们对应到内核里都是 task_struct。Linux 内核里的调度器,调度的对象就是 task_struct【这个数据结构统称为任务】
      在这里插入图片描述
      • 任务有优先级之分,Linux 系统为了保障高优先级的任务能够尽可能早的被执行,于是分为了这几种调度类:
        在这里插入图片描述
        • 完全公平调度(Completely Fair Scheduling):我们平日里遇到的基本都是普通任务,对于普通任务来说,公平性最重要,在 Linux 里面,实现了一个基于 CFS 的调度算法,也就是完全公平调度(Completely Fair Scheduling):让分配给每个任务的 CPU 时间是一样,于是它为每个任务安排一个虚拟运行时间 vruntime,如果一个任务在运行,其运行的越久,该任务的 vruntime 自然就会越大,而没有被运行的任务,vruntime 是不会变化的。那么,在 CFS 算法调度的时候【CFS 调度吗,它是会优先选择 vruntime 少的任务进行调度,所以高权重的任务就会被优先调度了,于是高权重的获得的实际运行时间自然就多了。】,会优先选择 vruntime 少的任务,以保证每个任务的公平性
        • CPU 运行队列:一个系统通常都会运行着很多任务,多任务的数量基本都是远超 CPU 核心数量,因此这时候就需要排队。事实上,每个 CPU 都有自己的运行队列(Run Queue, rq),用于描述在此 CPU 上所运行的所有进程,其队列包含三个运行队列,Deadline 运行队列 dl_rq、实时任务运行队列 rt_rq 和 CFS 运行队列 csf_rq,其中 csf_rq 是用红黑树来描述的,按 vruntime 大小来排序的,最左侧的叶子节点,就是下次会被调度的任务
          在这里插入图片描述
        • 调整优先级:如果我们启动任务的时候,没有特意去指定优先级的话,默认情况下都是普通任务,普通任务的调度类是 Fail,由 CFS 调度器来进行管理。CFS 调度器的目的是实现任务运行的公平性,也就是保障每个任务的运行的时间是差不多的。如果你想让某个普通任务有更多的执行时间,可以调整任务的 nice 值,从而让优先级高一些的任务执行更多时间。nice 的值能设置的范围是 -20~19, 值越低,表明优先级越高,因此 -20 是最高优先级,19 则是最低优先级,默认优先级是 0。【nice 值并不是表示优先级,而是表示优先级的修正数值,它与优先级(priority)的关系是这样的:priority(new) = priority(old) + nice。内核中,priority 的范围是 0~139,值越低,优先级越高,其中前面的 0~99 范围是提供给实时任务使用的,而 nice 值是映射到 100~139,这个范围是提供给普通任务用的,因此 nice 值调整的是普通任务的优先级。权重值与 nice 值的关系的,nice 值越低,权重值就越大,计算出来的 vruntime 就会越少,由于 CFS 算法调度的时候,就会优先选择 vruntime 少的任务进行执行,所以 nice 值越低,任务的优先级就越高。】

老规矩,图这道菜咱们是不能少的。
在这里插入图片描述

6.打手的生命状态(周期)

那既然进程提供了闯关的场景,让各个打手大显身手,施展自己的武艺。那么打手们玩的兵器、用的功夫肯定大不相同,所以,上菜。
在这里插入图片描述
屏幕前的观众小胡和敏小言(观众来源可以看看前面JVM的双亲委派机制那里JVM的双亲委派机制)看不下去了。说:你上的这道菜不全呀,你看菜里面有打手状态、分类啥的,你给咱上上来看看呀。
掰急呀,这就来。
在这里插入图片描述
打手出生->打手带着配备的兵器和学到的手艺去闯关做任务(打手做任务过程中会遇到三个挫折,幸亏菩萨给了三根毫毛才能顺利活到最后)->打手完成任务退出圈子。那咱们具体看看这个过程到底是怎样…(彩蛋,window+R,输入jconsole可以打开java线程的图形化监视界面,监视线程们的运行状态
在这里插入图片描述

  • 线程的生命状态:
    • 从Thread.State枚举或者说,分为六种状态。或者说在 Java 中线程的生命周期中一共有 6 种状态。(相当于在java中是区分不开运行状态和可运行状态的)【生活状态其实是别人眼中的不同一个变量不同的值而已,细思极恐有木有…】
      //一个枚举类
      public enum State{
      	NEW,//初始状态,线程被创建出来但没有被调用 start()
      	RUNNABLE,//运行状态,线程被调用了 start()等待运行的状态
      	BLOCKED,//阻塞状态,需要等待锁释放
      	WAITING,//等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)
      	TIMED_WAITING,//超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待
      	TERMINATED;//终止状态,表示该线程已经运行完毕
      }
      
      在这里插入图片描述
      • 如果想要确定线程当前的状态,可以通过 getState() 方法,并且线程在任何时刻只可能处于 1 种状态。【从OS层面描述,有五种:新建、可运行、运行、阻塞、终止】,具体状态如下:
        在这里插入图片描述
        在这里插入图片描述
        • NEW:当咱们通过 new实例化出一个 Thread后线程进入了新建状态; 调用start() 会执行线程的相应准备工作【当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;】,然后自动执行 run() 方法的内容,(调用 start() 方法【 调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态】,会启动一个线程并使线程进入了就绪状态当分配到时间片后就可以开始运行了【可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。)这是真正的多线程工作【🕴为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法
          在这里插入图片描述
          • 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态,为什么 JVM 没有区分这两种状态呢?
            • 现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了
          • run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。直接执行 run() 方法会把 run 方法当成一个 main 线程下的普通方法去执行并不会在某个线程中执行它,所以这并不是多线程工作。
            • 当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中
          • 打手的故事可是个令人心寒的故事呀:
            • 说到打手的出生,其实大家都一样嘛,生下来,被起名字,被”给“性别啥的。(只是实例化出来还没有调用执行start方法执行线程。或者说 当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,所以也没有开始执行 run() 方法里面的代码,那么此时它的状态就是 New。)
            • 打手长着长着长到了一米八,然后自己划着船去岛上学了武,学完之后师傅告诉他,你快回去吧,回去了不要告诉别人我是你的师傅哦。(调用start方法,做好被CPU调度的准备,但是不是说调用start方法后就会立即被执行)【[运行状态]指获取了CPU时间片的正在运行中的线程状态。当CPU时间片用完,会从[运行状态]转换至[可运行状态],会导致线程的上下文切换
              • 一旦线程调用了 start(),它的状态就会从 New 变成 Runnable:RUNNABLE是当调用了start()方法之后的线程状态,注意, (由于 BIO导致的线程阻塞,在Java里无法区分,仍认为是可运行)
        • Java 中的 Runable 状态对应操作系统线程状态中的两种状态,分别是 Running 和 Ready,也就是说,Java 中处于 Runnable 状态的线程有可能正在执行,也有可能没有正在执行,正在等待被分配 CPU 资源。所以,如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。
          在这里插入图片描述
        • 阻塞状态:在 Java 中阻塞状态通常不仅仅是 Blocked,实际上 Java 中阻塞状态它包括三种状态,分别是 Blocked(被阻塞)、Waiting(等待)、Timed Waiting(计时等待),这三种状态统称为阻塞状态
          • Blocked:从 Runnable 状态进入 Blocked 状态只有一种可能,就是进入 synchronized 保护的代码时没有抢到 monitor 锁
            在这里插入图片描述
            • 当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态
            • 想要从 Blocked 状态进入 Runnable 状态,要求线程获取 monitor 锁
            • Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll()
          • Waiting 等待:当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态线程进入 Waiting 状态有三种可能性
            在这里插入图片描述
            • Blocked 仅仅针对 synchronized monitor 锁,可是在 Java 中还有很多其他的锁,比如 ReentrantLock,如果线程在获取这种锁时没有抢到该锁就会进入 Waiting 状态,因为本质上它执行了 LockSupport.park() 方法,所以会进入 Waiting 状态。同样,Object.wait() 和 Thread.join() 也会让线程进入 Waiting 状态
            • 从 Waiting 状态流转到其他状态则比较特殊,因为首先 Waiting 是不限时的,也就是说无论过了多长时间它都不会主动恢复。可以分为下面两种情况:
              • 只有当执行了 LockSupport.unpark(),或者 join 的线程运行结束,或者被中断时才可以进入 Runnable 状态。
                在这里插入图片描述
              • 如果其他线程调用 notify() 或 notifyAll()来唤醒它,它会直接进入 Blocked 状态,这是为什么呢?因为唤醒 Waiting 线程的线程如果调用 notify() 或 notifyAll(),要求必须首先持有该 monitor 锁,所以处于 Waiting 状态的线程被唤醒时拿不到该锁,就会进入 Blocked 状态,直到执行了 notify()/notifyAll() 的唤醒它的线程执行完毕并释放 monitor 锁,才可能轮到它去抢夺这把锁,如果它能抢到,就会从 Blocked 状态回到 Runnable 状态
                在这里插入图片描述
          • Timed Waiting 限期等待:TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态
            在这里插入图片描述
            • 同样在 Timed Waiting 中执行 notify() 和 notifyAll() 也是一样的道理,它们会先进入 Blocked 状态,然后抢夺锁成功后,再回到 Runnable 状态
              在这里插入图片描述
            • 当然对于 Timed Waiting 而言,如果它的超时时间到了且能直接获取到锁/join的线程运行结束/被中断/调用了LockSupport.unpark(),会直接恢复到 Runnable 状态,而无需经历 Blocked 状态。
              在这里插入图片描述
        • 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态,要想进入这个状态有两种可能。
          • run() 方法执行完毕,线程正常退出。
          • 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。

打手阻塞的故事:这里面主要就是打手闯关的时候关口都是一夫当关万夫莫开,所以每次只有一个打手可以去打,其余的只能在门外先排个队等着。
队伍中的第二个打手受不了了,肿么肥事嘛,这么长时间咋还没过去。里面的正在占着关卡闯关的打手大声向外边喊着,别站着急呀,我这不是占着还没弄完呢嘛,我没弄完你其他人就不能进来哦
况且,我闯完,下一个也是人家坐在贴着同步队列标志的、那个离门最近的那个椅子上的那个哥们进来执行呀,你这还在队伍中第二个位置呢你急啥。
这个挑事的对手灰溜溜的转头走了,坐到了离门比较远的那排椅子上的第一个位置上。

正在里面闯关的打手心想,我容易嘛我,进了关卡后谁知道还有三个里屋(代表🕴线程的三种阻塞状态(当线程因为某种原因放弃 CPU 使用权后,即让出了 CPU 时间片,暂时就会停止运行,直到线程进入可运行状态(Runnable),才有机会再次获得 CPU 时间片转入 RUNNING 状态。一般来讲,从Java层面讲阻塞的情况可以分为如下三种(因为从OS层面讲只有五种状态,而且阻塞状态也只有一种):))呢,谁知道进哪个呀?不管了,乱敲一个门试试吧。

  • 处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行 ,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种
    • 同步阻塞(lock -> 锁池):线程在 获取synchronized同步锁失败( 因为锁被其它线程所占用),它会进入同步阻塞状态(RUNNING 状态的线程在获取对象的同步锁时,若该 同步锁被其他线程占用,则 JVM 将该线程放入锁池(lock pool)中
    • 等待阻塞(Object.wait -> 等待队列):运行状态中的线程 执行wait()方法,使本线程进入到等待阻塞状态(RUNNING 状态的线程执行 Object.wait() 方法后,JVM 会将线程放入等待序列(waitting queue)
    • 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。

他敲了一个门上贴着synchronized的里屋,里面有人喊了一身,别敲了有人…

  • 原来还有打手在关卡里面,谁知道里面还有比他还先到的,人家已经进入里屋了,把门给锁了,他也只能在里屋外面等,人家里屋里面的打手执行完任务把钥匙扔出来他才可以把里屋门打开进入里屋去。
    在这里插入图片描述

这个打手又敲了第二个里屋的门,里面的人说:

  • 由于我执行了下面图中左边红框中那些方法,所以这个屋被我占了,别人就不能进来了,包括你。
  • 开门的方法就是右边那些红框,你仔细看看,时机成熟自会有高人帮你开门。
    在这里插入图片描述
    打手看了一会
    十分钟后…
    打手说,算了,你这估计还得等一会呢,我先去下个屋看看吧
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    还没等打手走到第三个里屋门口呢,里面从门缝里扔出来一幅图。
    打手说:好好好,你不用说了,是不是你执行了上面这俩方法,把这件房间占了
Object.wait()
Thread.join()

我必须实现下面三件事才能开这间门,然后,等你出来,我再进去。
里面飞出来俩字…嗯呐
你们这都是啥规矩呀,真奇怪。
打手实现受不了了,就说,不行,你今天必须给我说一下你这XXX.wait()到底发生了什么,凭什么你执行一下这个就表示你占了这块一亩三分地呢?
里面那个又继续说,嗯呐。第三个屋是这样的,给你再看幅这第三间屋的设计原理图你就懂了。
在这里插入图片描述
打手:你上面那个第二个还没展开哟,你别想蒙我
嗯呐:这就来这就来在这里插入图片描述
打手说,你们这也太复杂了吧,那你们是不是和我一样,有生有终(线程死亡的三种方式)呢?
嗯呐:那肯定呀,上菜

  • 🕴线程死亡的三种方式
    • 正常结束:run() 或者 call() 方法执行完成后,线程正常结束
    • 异常结束:线程抛出一个未捕获的 Exception 或 Error,导致线程异常结束
    • 调用 stop():直接调用线程的 stop() 方法来结束该线程,但是一般不推荐使用该种方式,因为该方法通常容易导致死锁

在这里插入图片描述
嗯呐:这样吧,看你在外边等了半天了,给你点吃的,你先享用,我们三个里屋你可能得得好一会了,唠唠其他的。

  • Java中如何保证线程顺序执行:线程开始的顺序跟执行的顺序是不一样的。如果只是创建三个线程然后执行,最后的执行顺序是不可预期的。这是因为在创建完线程之后,线程执行的开始时间取决于CPU何时分配时间片,线程可以看成是相对于的主线程的一个异步操作
    在这里插入图片描述
    • 使用Thread.join()实现:
      在这里插入图片描述
    • 使用单线程线程池来实现:另一种保证线程顺序执行的方法是使用一个单线程的线程池,这种线程池中只有一个线程,相应的,内部的线程会按加入的顺序来执行
      在这里插入图片描述
    • 使用volatile关键字修饰的信号量实现:线程可以无序运行,但是执行结果按顺序执行。三个线程都被创建并start(),这时候三个线程随时都可能执行run()方法。因此为了保证run()执行的顺序性,我们肯定需要一个信号量来让线程知道在任意时刻能不能执行逻辑代码。另外,因为三个线程是独立的,这个信号量的变化肯定需要对其他线程透明,因此volatile关键字也是必须要的。
      在这里插入图片描述
      在这里插入图片描述
    • 使用Lock和信号量实现:此种方法的思想跟第三种方法是一样的,都是不考虑线程执行的顺序而是考虑用一些方法控制线程执行业务逻辑的顺序
      在这里插入图片描述
      在这里插入图片描述
  • Java中线程分为两类:daemon线程(守护线程)与用户线程
    • 守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
      //Java中创建一个守护线程:
      public static void main(String[] args){
      	Thread daemonThread = new Thread(new Runnable(){
      		@Override
      		public void run(){
      			......
      		}
      	});
      	//设置为守护线程只需要设置线程的daemon参数为true就行
      	daemonThread.setDaemon(true);
      	daemonThread.start();
      } 
      
      • 当咱们 JVM启动时会调用main函数,启动一个main函数就相当于启动了一个JVM的进程,而main函数所在的线程就是这个JVM的进程中的一个线程
    • 守护线程与用户线程的区别就是:
      • 当最后一个非守护线程结束时JVM会正常退出,也就是只要还有一个用户线程还没结束,正常情况下JVM就不会退出
      • 而不管当前是否有守护线程,守护线程是否结束并不影响JVM的退出
        在这里插入图片描述
        在这里插入图片描述
        打手便坐在门外吃了起来

打手边吃边说,给你们夫妻俩唠个小故事,之前我们打手之间发生过一次互夺兵器案件(你看上我的兵器了,我又觉得你的兵器好用:死锁(多个线程由于互相等待对方持有的资源而同时被阻塞,它们中的一个或者全部都在等待某个资源被释放从而导致谁都没法执行。由于线程被无限期地阻塞,因此程序不可能正常终止。 )),我差点没命了(🕴 什么是线程死锁?如何避免死锁?)—线程死锁的补充篇
在这里插入图片描述
小胡和敏小言:快聊聊

  • 检测死锁可以使用jconsole.工具,或者使用jps定位进程id,再用jstack定位死锁:
  • 死锁的产生必须必备的四个必要条件(或者说你如果是个死锁你肯定会满足这四个条件)
    • 互斥条件:该资源任意一个时刻只由一个线程占用(其他线程此时还想请求获取该资源,必须等待占有资源的线程释放该资源才能得到该资源)
    • 请求与保持条件:指一个线程自己已经持有了至少一个资源,但是自己吃着碗子的看着锅里的,又想请求获取新的资源,但此时这个新资源已经被其他线程占有,所以这个请求与保持条件限定此时当前线程阻塞不能硬抢别人的不能释放自己已经获取的至少一个的资源)。
    • 不可剥夺条件(抢占):线程已获得的资源在末使用完之前不能被其他线程强行剥夺只有自己使用完毕后才释放资源
    • 循环等待条件:指的是发生死锁时,肯定是若干进程之间形成一种头尾相接的循环等待资源关系。(每个进程都等待它前一个进程所持有的资源,即线程集合{T0,T1,T2,…,Tn}中 T0 正在等待一 个T1占用的资源,T1正在等待T2占用的资源,……Tn 在等待己被 T0占用的资源。)
  • 如何避免线程死锁?(只要 破坏产生死锁的四个条件中的其中一个【目前只有请求与保持条件和环路等待条件是可以被破坏的】 就可以了)
    • 破坏互斥条件 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
    • 一次性申请线程所有需要的资源----破坏请求与保持条件。
    • 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以 主动释放它占有的资源(进程回滚+死锁检测)。----破坏不剥夺条件
    • 按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放----破坏循环等待条件和请求与保持条件
    • 指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作。通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。【按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件】
      • 比如,破坏了破坏循环等待条件, 线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
    • 使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁
    • 避免死锁就是 在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态
      • 安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3…Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3…Pn> 序列为安全序列

小胡和敏小言:啊,完了,这就完了,还有没有呀?
打手,估计线程就到这了,下篇就该进程了…翻了个身睡了过去
小胡:梦话?
敏小言:估计是,都翻B面了都。老公,那咱们也去吃饭吧,吃完来再看。
走喽…

巨人的肩膀:
低编程并发(微信公众号这位老师)
B站的狂神说老师
Java并发编程之美
Java19 带来了一个 Java 开发者垂涎已久的新特性—— 虚拟线程。在 Java19 中,之前我们常用的线程叫做平台线程(platform thread),与系统内核线程仍然是一一对应的。其中大量(M)的虚拟线程在较小数量(N)的平台线程(与操作系统线程一一对应)上运行(M:N调度)。多个虚拟线程会被 JVM 调度到某一个平台线程上执行,一个平台线程同时只会执行一个虚拟线程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值