精华内容
下载资源
问答
  • 2021-06-02 22:15:05

    进程的阻塞是指使一个进程让出处理器,去等待一个事件,如等待资源、等待I/O完成、等待一个事件发等,通常进程自己调用阻塞原语阻塞自己,所以,是进程自主行为,是1个同步事件。当一个等待事件结束会产生一个中断,从而,激活操作系统,在系统的控制之下将被阻塞的进程唤醒,如I/O操作结束、某个资源可用或期待事件出现。进程的阻塞和唤醒显然是由进程切换来完成。

    更多相关内容
  • 来源:SegmentFault 思否社区作者:byte进程我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会装载到内存中,接着 CPU 会执行程序中的每一条...

    来源:SegmentFault 思否社区

    作者:byte


    进程

    我们编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当我们运行这个可执行文件后,它会被装载到内存中,接着 CPU 会执行程序中的每一条指令,那么这个运行中的程序,就被称为「进程」。


    现在我们考虑有一个会读取硬盘文件数据的程序被执行了,那么当运行到读取文件的指令时,就会去从硬盘读取数据,但是硬盘的读写速度是非常慢的,那么在这个时候,如果 CPU 傻傻的等硬盘返回数据的话,那 CPU 的利用率是非常低的。


    当进程要从硬盘读取数据时,CPU 不需要阻塞等待数据的返回,而是去执行另外的进程。当硬盘数据返回时,CPU 会收到个中断,于是 CPU 再继续运行这个进程。

    900f7ebcc3179402e300f601fa534a9a.png

    这种多个程序、交替执行的思想,就有 CPU 管理多个进程的初步想法。

    对于一个支持多进程的系统,CPU 会从一个进程快速切换至另一个进程,其间每个进程各运行几十或几百个毫秒。

    虽然单核的 CPU 在某一个瞬间,只能运行一个进程。但在 1 秒钟期间,它可能会运行多个进程,这样就产生并行的错觉,实际上这是并发


    进程的状态

    进程有着「运行 - 暂停 - 运行」的活动规律。一般说来,一个进程并不是自始至终连续不停地运行的,它与并发执行中的其他进程的执行是相互制约的。

    它有时处于运行状态,有时又由于某种原因而暂停运行处于等待状态,当使它暂停的原因消失后,它又进入准备运行状态。

    所以,在一个进程的活动期间至少具备三种基本状态,即运行状态、就绪状态、阻塞状态。5054b056ca561a4b9fd2a9b2f775a93b.png

    上图中各个状态的意义:

    • 运行状态(_Runing_):该时刻进程占用 CPU;
    • 就绪状态(_Ready_):可运行,但因为其他进程正在运行而暂停停止;
    • 阻塞状态(_Blocked_):该进程正在等待某一事件发生(如等待输入/输出操作的完成)而暂时停止运行,这时,即使给它CPU控制权,它也无法运行;

    当然,进程另外两个基本状态:

    • 创建状态(_new_):进程正在被创建时的状态;
    • 结束状态(_Exit_):进程正在从系统中消失时的状态;

    于是,一个完整的进程状态的变迁如下图:

    7bf311d9ea95a534f7bce8a7122d1c70.png

    • _NULL -> 创建状态_:一个新进程被创建时的第一个状态;
    • _创建状态 -> 就绪状态_:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
    • _就绪态 -> 运行状态_:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
    • _运行状态 -> 结束状态_:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
    • _运行状态 -> 就绪状态_:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
    • _运行状态 -> 阻塞状态_:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
    • _阻塞状态 -> 就绪状态_:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;

    挂起状态概念 挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态

    7bab080dcdc8797a9368879569258f36.png


    进程的控制结构

    在操作系统中,是用进程控制块(_process control block,PCB_)数据结构来描述进程的。


    PCB 是进程存在的唯一标识,这意味着一个进程的存在,必然会有一个 PCB,如果进程消失了,那么 PCB 也会随之消失

    PCB包含:

    • 进程状态:状态可以包括新的、就绪、运行、等待、停止等。
    • 程序计数器:计数器表示进程将要执行的下个指令的地址。
    • CPU 寄存器:根据计算机体系结构的不同,寄存器的类型和数量也会不同。它们包括累加器、索引寄存器、堆栈指针、通用寄存器和其他条件码信息寄存器。在发生中断时,这些状态信息与程序计数器一起需要保存,以便进程以后能正确地继续执行。
    • CPU 调度信息:这类信息包括进程优先级、调度队列的指针和其他调度参数。
    • 内存管理信息:根据操作系统使用的内存系统,这类信息可以包括基地址和界限寄存器的值、页表或段表。
    • 记账信息:这类信息包括 CPU 时间、实际使用时间、时间期限、记账数据、作业或进程数量等。
    • I/O 状态信息:这类信息包括分配给进程的 I/O 设备列表、打开文件列表等。


    PCB组织方式

    通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列。比如:

    • 将所有处于就绪状态的进程链在一起,称为就绪队列;
    • 把所有因等待某事件而处于等待状态的进程链在一起就组成各种阻塞队列;
    • 另外,对于运行队列在单核 CPU 系统中则只有一个运行指针了,因为单核 CPU 在某个时间,只能运行一个程序。

    那么,就绪队列和阻塞队列链表的组织形式如下图:

    de70879425c2adce2fdc54cdf20bc5ed.png

    创建进程

    操作系统允许一个进程创建另一个进程,而且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程时同时也会终止其所有的子进程。

    创建进程的过程如下:

    • 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB,PCB 是有限的,若申请失败则创建失败;
    • 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源;
    • 初始化 PCB;
    • 如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行;
    终止进程

    进程可以有 3 种终止方式:正常结束、异常结束以及外界干预(信号 kill 掉)。

    终止进程的过程如下:

    • 查找需要终止的进程的 PCB;
    • 如果处于执行状态,则立即终止该进程的执行,然后将 CPU 资源分配给其他进程;
    • 如果其还有子进程,则应将其所有子进程终止;
    • 将该进程所拥有的全部资源都归还给父进程或操作系统;
    • 将其从 PCB 所在队列中删除;
    阻塞进程

    当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。而一旦被阻塞等待,它只能由另一个进程唤醒。

    阻塞进程的过程如下:

    • 找到将要被阻塞进程标识号对应的 PCB;
    • 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;
    • 将该 PCB 插入的阻塞队列中去;
    唤醒进程

    进程由「运行」转变为「阻塞」状态是由于进程必须等待某一事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒自己的。

    如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。

    唤醒进程的过程如下:

    • 在该事件的阻塞队列中找到相应进程的 PCB;
    • 将其从阻塞队列中移出,并置其状态为就绪状态;
    • 把该 PCB 插入到就绪队列中,等待调度程序调度;

    进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。

    进程上下文切换

    进程是由内核管理和调度的,所以进程的切换只能发生在内核态。

    所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

    通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:

    8a5eee6c776705c061b1d20fde99543e.png

    进程上下文切换的场景

    • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行;
    • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
    • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
    • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
    • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

    参考文章:

    https://mp.weixin.qq.com/s/wn8w14Bkf99zbxcGQr30Zg

    http://c.biancheng.net/view/1202.html


    - END -

    b9ef254d67735ba01d0fa0d1b0855fa1.png

    942307bafacdce010db22bea6086d582.gif

    展开全文
  • 进程简介 并发和并行并发:在一个时间段中多个程序都启动运行在用一个处理机中并行:两个进程分别由不同的CPU管理执行,两个进程不抢占CPU的资源,且可以同时运行,叫做并行区别在于是否同时多进程的优点各个进程有...

    进程简介

    并发和并行

    • 并发:在一个时间段中多个程序都启动运行在用一个处理机中

    • 并行:两个进程分别由不同的CPU管理执行,两个进程不抢占CPU的资源,且可以同时运行,叫做并行

    区别在于是否同时

    多进程的优点

    • 各个进程有自己的内存空间,所以具有更强的容错率,不至于一个错了导致系统崩溃

    • 具有更好的多核可伸缩性,因为进程将地址空间,页表等进行了隔离,在多核的系统上可伸缩更强。

    同步、异步、阻塞、非阻塞

        同步异步、阻塞非阻塞是两个不同层面的问题,一个是operation层一个是kernal层。

    同步和异步最大的区别就是是否需要底层的响应再执行。

    阻塞和非阻塞最大的区别就是能否立即给出响应

    • 同步:当一个同步调用发送后,调用者要一直等待返回结果。通知后,才能进行后续的执行。

    • 异步:当一个异步过程调用发出后,调用者不能立刻得到返回结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

    • 阻塞:是指调用结果返回前,当前线程会被挂起,即阻塞。

    • 非阻塞:是指即使调用结果没返回,也不会阻塞当前线程。

    进程的状态与变迁

    进程有五种状态:

    ab045687676cfd2983efd85d6bc84252.png
    image
    • 运行状态:该进程占用CPU

    • 就绪状态:可运行,但是由于其他进程正在运行而暂停

    • 阻塞状态:该进程正在等待某一事件发生而暂停运行,此时,即使给他CPU控制权,它也无法运行

    • 创建状态:进程正在被创建时的状态

    • 结束状态:进程正在从系统中消失的状态。

    挂起状态:表示进程没有占有物理内存空间。

    • 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现

    • 就绪挂起状态:进程在外存(硬盘),但只要进入内存,就立即运行

    0e0154c2e5126d6a9cc863c4bb7f51f1.png
    image

    线程变迁

    • NULL->创建状态:一个新进程被创建时的第一个状态

    • 创建状态->就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,就变为就绪状态,一般来说很快

    • 就绪状态->运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给CPU正式运行该进程

    • 运行状态->结束状态:当进程已经完成或出错时,会被操作系统做结束状态处理

    • 运行状态->就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;

    • 运行状态->阻塞状态:当进程请求某个事件且必须等待时,例如请求I/O

    • 阻塞状态->就绪状态:当进程要等待的事件完成时,它就从阻塞状态变为就绪状态。

    进程的控制结构

        在操作系统中,是用进程控制块(PCB)数据结构来描述进程的

        PCB是进程存在的唯一表示,这意味着一个进程的存在,必然会有一个PCB,如果进程消失了,那么PCB也会随着消失。

    PCB包含的信息

    • 进程描述信息

      • 进程标识符:表示各个进程,每个进程都有一个并且唯一的标识符

      • 用于标识符:进程归属的用户,用户标识符主要为共享和保护服务

    • 进程控制和管理信息

      • 进程当前的状态,如:new、ready、running、waiting或blocked

      • 进程优先级:进程抢占CPU时的优先级

    • 资源分配清单

      • 有关内存地址空间或虚拟地址空间的信息,所打开文件的列表和所使用的I/O设备信息

    • CPU相关信息

      • CPU中各个寄存器的值,当进程被切换时,CPU的状态信息都会被保存在相应的PCB中,以便进程重新执行时,能从断点处继续执行。

    PCB如何组织

        通常是通过链表的方式进行组织,把具有相同状态的进程链在一起,组成各种队列,比如:

    • 将所有处于就绪状态的进程链在一起,称为就绪队列

    • 把所有因等待某事件而处于等待状态的进程链在一起组成各种阻塞队列

    • 另外,对于运行队列在单核CPU系统中则只有一个运行指针了,因为单核CPU在某个时间只能运行一个程序。

    就绪队列和阻塞队列链表的组织形式如下所示:

    354e9e222150cb038bc7fae840593713.png
    image

        另外除了链接的组织方式,还有索引方式,它的工作原理:将同一状态的进程组织在一个索引表中,索引表项指向响应的PCB,不同状态对应不同的索引表。

        一般来说会选择链表,因为可能面临创建、销毁等调度导致进程状态发生变化,所以链表能更加灵活的插入和删除

    进程的控制

        当我们熟悉了进程的状态变迁和进程的数据结构PCB后,再来看看进程的创建、终止、阻塞、唤醒的过程,这些过程也就是进程的控制

    创建进程

        操作系统运行一个进程创建另一个进程,而且运行子进程继承父进程所拥有的资源,当子进程被终止时,其父进程处继承的资源应当还给父进程。同时终止父进程时也同时会终止所有的子进程

    创建进程的过程:

    • 为新进程分配一个唯一的进程标识号,并申请一个空白的PCB,PCB是有限的,若申请失败则创建失败

    • 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源

    • 初始化PCB

    • 如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行

    终止进程

        进程终止的方式有三种:正常结束、异常结束和外键干预(直接kill掉)

    终止进程的过程如下:

    • 查找需要终止的进程的PCB

    • 如果处于执行状态,则立即终止改进程的执行,然后将CPU资源分配给其他进程

    • 如果其还有子进程,则该将所有子进程终止

    • 将该进程所拥有的全部资源都归还给父进程或操作系统

    • 将其从PCB所在队列中删除

    阻塞进程

        当进程需要等待某一事件完成时,它可以调用阻塞语句把自己阻塞等待。一旦被阻塞等待,它就只能由另一进程唤醒

    阻塞进程的过程:

    • 找到将要被阻塞进程标识号对应的 PCB;

    • 如果该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行;

    • 将该 PCB 插入的阻塞队列中去;

    唤醒进程

        进程由运行转变为阻塞状态是由于进程必须等待某一事件的完成,所以出于阻塞状态的进程是绝对不可能叫醒自己的。

        如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发现者进程用唤醒语句叫醒它。

    唤醒进程的过程如下:

    • 在该事件的阻塞队列中找到相应进程的 PCB;

    • 将其从阻塞队列中移出,并置其状态为就绪状态;

    • 把该 PCB 插入到就绪队列中,等待调度程序调度;

      进程的阻塞和唤醒是一对功能相反的语句,如果某个进程调用了阻塞语句,则必有一个与之对应的唤醒语句。

    进程的上下文切换

        各个进程之间是共享CPU资源的,在不同的时候进程之间需要切换,让不同的进程可以在CPU执行,那么一个进程切换到另一个进程运行,称为进程的上下文切换。

    CPU上下文切换

        由于任务是交给CPU运行的,那么在每个任务运行前,CPU需要知道任务从哪里加载,又从哪里开始运行

        所以,操作系统需要事先帮CPU设置好CPU寄存器和程序计数器。(CPU上下文)

    • CPU寄存器是CPU中一个容量小,速度快的内存

    • 程序计数器:则是用来存储CPU正在执行的指令位置、或者即将执行的下一条指令位置。

      CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

        任务主要包括进程、线程和中断。所以,可以根据任务的不同,把CPU上下文切换分为:进程上下文切换、线程上下文切换和中断上下文切换

    进程的上下文切换

        进程是由内核管理和调度的,所以进程的切换只能发生在内核态,所以进程的上下文切换不仅包含了虚拟内存、栈、全量变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

        通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:

    d2b3ee8c7028e9c9701df2d4fade4e2a.png
    image

    发生进程上下文切换的场景

    • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行;

    • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;

    • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;

    • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;

    • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;

    进程间通信

    进程调度算法

    线程

    最后

    • 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持

    • 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我

    • 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。

    8d575fce97a49a894d96ee646a9d3ac7.png
    image
    展开全文
  • 线程与进程阻塞 线程阻塞 线程在运行的过程中因为某些...此时引起进程调度,OS把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为阻塞状态。 进程的挂起 挂起进程在操作系统中可以定义为暂

    线程与进程的阻塞

    线程阻塞
    线程在运行的过程中因为某些原因而发生阻塞,阻塞状态的线程的特点是:该线程放弃CPU的使用,暂停运行,只有等到导致阻塞的原因消除之后才回复运行,或者是被其他的线程中断,该线程也会退出阻塞状态,同时抛出InterruptedException。
    进程阻塞
    正在执行的进程由于发生某时间(如I/O请求、申请缓冲区失败等)暂时无法继续执行。此时引起进程调度,OS把处理机分配给另一个就绪进程,而让受阻进程处于暂停状态,一般将这种状态称为阻塞状态。

    进程的挂起

    挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态即就绪态,系统在超过一定的时间没有任何动作。
    对于线程来说,挂起是没有的状态,因为进程的资源是线程共享的,所以进程的挂起就代表了线程的挂起

    共同点:
    1. 进程都暂停执行
    2. 进程都释放CPU,即两个过程都会涉及上下文切换

    不同点:
    1. 对系统资源占用不同:虽然都释放了CPU,但阻塞的进程仍处于内存中,而挂起的进程通过“对换”技术被换出到外存(磁盘)中。
    2. 发生时机不同:阻塞一般在进程等待资源(IO资源、信号量等)时发生;而挂起是由于用户和系统的需要,例如,终端用户需要暂停程序研究其执行情况或对其进行修改、OS为了提高内存利用率需要将暂时不能运行的进程(处于就绪或阻塞队列的进程)调出到磁盘
    3. 恢复时机不同:阻塞要在等待的资源得到满足(例如获得了锁)后,才会进入就绪状态,等待被调度而执行;被挂起的进程由将其挂起的对象(如用户、系统)在时机符合时(调试结束、被调度进程选中需要重新执行)将其主动激活

    7状态模型

    7状态模型中,和阻塞,挂起相关的有三个概念,阻塞状态,阻塞挂起状态,就绪挂起状态。它们的关系如下图:
    在这里插入图片描述

    阻塞态:进程在内存中并等待一个事件。
    阻塞/挂起态:进程在外存中并等待一个事件。
    就绪/挂起态:进程在外存中,但是只要被载入内存就可以执行。

    五状态到七状态模型增加了两个挂起状态的原因。
    对于I/O密集型的进程,一个进程进入等待(阻塞)状态后,处理器会转向处理另一个就绪的进程,但是由于处理器处理的速度相比I/O要快的多,所以可能会出现所有进程都处于阻塞状态的情况。导致处理器的效率低下。一种解决办法是扩充内存适应更多的进程。
    有以下缺点:
    1.内存的价格
    2.程序对内存空间需求的增长速度比内存价格下降的速度快。因此,更大的内存往往导致更大的进程,而不是更多的进程。

    另一种解决方案是交换。包括把内存中某个进程的一部分或全部移到磁盘中。当内存中没有处于就绪状态的进程时,操作系统就把被阻塞的进程患处到磁盘中的”挂起队列“(suspend queue),即暂时保存从内存中”驱逐“出来的被挂器的进程队列。操作系统再次之后取出挂起队列中的另一个进程,或者接受一个新进程的请求,将其纳入内存运行。于是就产生了挂起这样一个状态。

    睡眠状态没有在七状态模型中出现,其实它是阻塞或等待状态下的一种更细的分支。分支的依据是进程由运行状态进入阻塞状态的原因。睡眠是进程通过代理(自己或父进程)主动引起的进程调度,并且这种阻塞状态恢复到就绪状态的时间是确定的。而狭义上的阻塞可以理解为一个被动的动作。
    关于睡眠,有一篇博客是这样解释的
    当一个进程获取资源比如获取最普通的锁而失败后,可以有两种处理方式,
    1、自己睡眠,触发调度;
    2、忙等待,使用完自己的时间。所以从这里看,睡眠的确是一种主动的方式,且仅仅作为一种处理手段。当然睡眠不仅仅用于阻塞,更多的,我们可以在适当的时候设置让进程睡眠一定的时间,那么在这里,就可以发现,睡眠之前,我们已经预先规定了,你只能睡多长时间,这段时间过后,比必须返回来工作。

    阻塞的原因

    主要分为:线程中的阻塞、Socket客户端的阻塞、Socket服务器端的阻塞。

    一般线程中的阻塞:

    A、线程执行了Thread.sleep(int millsecond);方法,当前线程放弃CPU,睡眠一段时间,然后再恢复执行
    B、线程执行一段同步代码,但是尚且无法获得相关的同步锁,只能进入阻塞状态,等到获取了同步锁,才能回复执行。
    C、线程执行了一个对象的wait()方法,直接进入阻塞状态,等待其他线程执行notify()或者notifyAll()方法。
    D、线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。

    Socket客户端的阻塞:

    A、请求与服务器连接时,调用connect方法,进入阻塞状态,直至连接成功。
    B、当从Socket输入流读取数据时,在读取足够的数据之前会进入阻塞状态。比如说通过BufferedReader类使用readLine()方法时,在没有读出一行数据之前,数据量就不算是足够,会处在阻塞状态下。
    C、调用Socket的setSoLinger()方法关闭了Socket延迟,当执行Socket的close方法时,会进入阻塞状态,知道底层Socket发送完所有的剩余数据

    Socket服务器的阻塞:

    A、线程执行ServerSocket的accept()方法,等待客户的连接,直到接收到客户的连接,才从accept方法中返回一个Socket对象

    B、从Socket输入流读取数据时,如果输入流没有足够的数据,就会进入阻塞状态

    D、线程向Socket的输出流写入一批数据,可能进入阻塞状态

    挂起的原因

    (1)终端用户的请求。当终端用户在自己的程序运行期间发现有可疑问题时,希望暂停使自己的程序静止下来。亦即,使正在执行的进程暂停执行;若此时用户进程正处于就绪状态而未执行,则该进程暂不接受调度,以便用户研究其执行情况或对程序进行修改。我们把这种静止状态成为“挂起状态”。

    (2)父进程的请求。有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各子进程间的活动。
    (3)负荷调节的需要。当实时系统中的工作负荷较重,已可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
    (4)操作系统的需要。操作系统有时希望挂起某些进程,以便检查运行中的资源使用情况或进行记账。
    (5)对换的需要。为了缓和内存紧张的情况,将内存中处于阻塞状态的进程换至外存上。

    操作系统中睡眠、阻塞、挂起的区别形象解释:

     首先这些术语都是对于线程来说的。对线程的控制就好比你控制了一个雇工为你干活。你对雇工的控制是通过编程来实现的。
    
     挂起线程的意思就是你对主动对雇工说:“你睡觉去吧,用着你的时候我主动去叫你,然后接着干活”。
    
     使线程睡眠的意思就是你主动对雇工说:“你睡觉去吧,某时某刻过来报到,然后接着干活”。
    
     线程阻塞的意思就是,你突然发现,你的雇工不知道在什么时候没经过你允许,自己睡觉了,但是你不能怪雇工,因为本来你让雇工扫地,结果扫帚被偷了或被邻居家借去了,你又没让雇工继续干别的活,他就只好睡觉了。至于扫帚回来后,雇工会不会知道,会不会继续干活,你不用担心,雇工一旦发现扫帚回来了,他就会自己去干活的。因为雇工受过良好的培训。这个培训机构就是操作系统。
    

    线程栈状态

    线程栈状态有如下几种:

    1、NEW

    2、RUNNABLE

    3、BLOCKED

    4、WAITING

    5、TIMED_WAITING

    6、TERMINATED

    1、NEW

    线程刚刚被创建,也就是已经new过了,但是还没有调用start()方法,这个状态我们使用jstack进行线程栈dump的时候基本看不到,因为是线程刚创建时候的状态。

    2、RUNNABLE
    从虚拟机的角度看,线程正在运行状态,状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等。
    处于RUNNABLE状态的线程是不是一定会消耗cpu呢,不一定,像socket IO操作,线程正在从网络上读取数据,尽管线程状态RUNNABLE,但实际上网络io,线程绝大多数时间是被挂起的,只有当数据到达后,线程才会被唤起,挂起发生在本地代码(native)中,虚拟机根本不一致,不像显式的调用sleep和wait方法,虚拟机才能知道线程的真正状态,但在本地代码中的挂起,虚拟机无法知道真正的线程状态,因此一概显示为RUNNABLE。
    在这里插入图片描述

    3、BLOCKED

    线程处于阻塞状态,正在等待一个monitor lock。通常情况下,是因为本线程与其他线程公用了一个锁。其他在线程正在使用这个锁进入某个synchronized同步方法块或者方法,而本线程进入这个同步代码块也需要这个锁,最终导致本线程处于阻塞状态。
    在这里插入图片描述

    4、WAITING

    这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束,处于waiting状态的线程基本不消耗CPU。

    在这里插入图片描述
    5、TIMED_WAITING

    该线程正在等待,通过使用了 sleep, wait, join 或者是 park 方法。(这个与 WAITING 不同是通过方法参数指定了最大等待时间,WAITING 可以通过时间或者是外部的变化解除),线程等待指定的时间。

    sleep和wait的区别

    对于sleep()方法,我们首先要知道该方法是属于Thread类中的。
    而wait()方法,则是属于Object类中的。

    sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。这点很重要,要区别线程持有锁和让出cpu的区别

    而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

    从使用角度看,sleep是Thread线程类的方法,而wait是Object顶级类的方法。

    sleep可以在任何地方使用,而wait只能在同步方法或者同步块中使用。

    CPU及资源锁释放

    sleep,wait调用后都会暂停当前线程并让出cpu的执行时间,但不同的是sleep不会释放当前持有的对象的锁资源,到时间后会继续执行,而wait会放弃所有锁并需要notify/notifyAll后重新获取到对象锁资源后才能继续执行。

    sleep和wait的区别:

    1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
    2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
    3、它们都可以被interrupted方法中断。

    具体来说:

    Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

    wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。
    在这里插入图片描述
    在这里插入图片描述

    wait()和notify()的底层详解

    Java多线程开发中,我们常用到wait()和notify()方法来实现线程间的协作,简单的说步骤如下:

    1、A线程取得锁,执行wait(),释放锁;
    2、B线程取得锁,完成业务后执行notify(),再释放锁;
    3、B线程释放锁之后,A线程取得锁,继续执行wait()之后的代码;

    关于synchronize修饰的代码块
    通常,对于synchronize(lock){…}这样的代码块,编译后会生成monitorenter和monitorexit指令,线程执行到monitorenter指令时会尝试取得lock对应的monitor的所有权(CAS设置对象头),取得后即获取到锁,执行monitorexit指令时会释放monitor的所有权即释放锁;

    先用完整的demo程序来模拟场景吧,以下是源码:

    public class NotifyDemo {
    
        private static void sleep(long sleepVal){
            try{
                Thread.sleep(sleepVal);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    
        private static void log(String desc){
            System.out.println(Thread.currentThread().getName() + " : " + desc);
        }
    
        Object lock = new Object();
    
        public void startThreadA(){
            new Thread(() -> {
                synchronized (lock){
                    log("get lock");
                    startThreadB();
                    log("start wait");
                    try {
                        lock.wait();
                    }catch(InterruptedException e){
                        e.printStackTrace();
                    }
    
                    log("get lock after wait");
                    log("release lock");
                }
            }, "thread-A").start();
        }
    
        public void startThreadB(){
            new Thread(()->{
                synchronized (lock){
                    log("get lock");
                    startThreadC();
                    sleep(100);
                    log("start notify");
                    lock.notify();
                    log("release lock");
    
                }
            },"thread-B").start();
        }
    
        public void startThreadC(){
            new Thread(() -> {
                synchronized (lock){
                    log("get lock");
                    log("release lock");
                }
            }, "thread-C").start();
        }
    
        public static void main(String[] args){
            new NotifyDemo().startThreadA();
        }
    }
    
    

    以上就是本次实战用到的demo,代码功能简述如下:

    1、启动线程A,取得锁之后先启动线程B再执行wait()方法,释放锁并等待;
    2、线程B启动之后会等待锁,A线程执行wait()之后,线程B取得锁,然后启动线程C,再执行notify唤醒线程A,最后退出synchronize代码块,释放锁;
    3、线程C启动之后就一直在等待锁,这时候线程B还没有退出synchronize代码块,锁还在线程B手里;
    线程A在线程B执行notify()之后就一直在等待锁,这时候线程B还没有退出synchronize代码块,锁还在线程B手里;
    4、线程B退出synchronize代码块,释放锁之后,线程A和线程C竞争锁;

    把上面的代码在Openjdk8下面执行,反复执行多次,都得到以下结果:

    thread-A : get lock
    thread-A : start wait
    thread-B : get lock
    thread-C : c thread is start
    thread-B : start notify
    thread-B : release lock
    thread-A : after wait, acquire lock again
    thread-A : release lock
    thread-C : get lock
    thread-C : release lock

    针对以上结果,问题来了:
    第一个问题:
    将以上代码反复执行多次,结果都是B释放锁之后A会先得到锁,这又是为什么呢?C为何不能先拿到锁呢?

    第二个问题:
    线程C自开始就执行了monitorenter指令,它能得到锁是容易理解的,但是线程A呢?在wait()之后并没有没有monitorenter指令,那么它又是如何取得锁的呢?

    wait()、notify()这些方法都是native方法,所以只有从JVM源码寻找答案了,本次阅读的是openjdk8的源码;

    针对问题:

    线程A在wait()的时候做了什么?
    线程C启动后,由于此时线程B持有锁,那么线程C此时在干啥?
    线程B在notify()的时候做了什么?
    线程B释放锁的时候做了什么?

    在源码中有段注释堪称是整篇文章最重要的说明,请大家始终记住这段信息,处处都用得上:

    ObjectWaiter对象存在于WaitSet、EntryList(cxq)等集合中,或者正在在这俩个集合中互相移动

    原文如下:
    在这里插入图片描述
    线程A在wait()的时候做了什么
    打开hotspot/src/share/vm/runtime/objectMonitor.cpp,看ObjectMonitor::wait方法:
    在这里插入图片描述
    如上图所示,有两处代码值得我们注意:

    1、绿框中将当前线程包装成ObjectWaiter对象,并且状态为TS_WAIT,这里对应的是jstack看到的线程状态WAITING;
    2、红框中调用了AddWaiter方法,跟进去看下:

    在这里插入图片描述
    这个ObjectWaiter对象被放入了_WaitSet中,_WaitSet是个环形双向链表(circular doubly linked list)

    回到ObjectMonitor::wait方法接着往下看,会发现关键代码如下图,当前线程通过park()方法开始挂起(suspend):
    在这里插入图片描述
    至此,我们把wait()方法要做的事情就理清了:

    1、包装成ObjectWaiter对象,状态为TS_WAIT;
    2、ObjectWaiter对象被放入锁对象的_WaitSet中;
    3、当前线程挂起;

    然后b线程执行到monitorenter指令时会尝试取得lock对应的monitor的所有权(CAS设置对象头)

    线程B持有锁的时候线程C在干啥
    此时的线程C无法进入synchronized{}代码块,用jstack看应该是BLOCKED状态,如下图:
    在这里插入图片描述
    我们看看monitorenter指令对应的源码吧,位置:openjdk/hotspot/src/share/vm/interpreter/interpreterRuntime.cpp

    IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
      if (PrintBiasedLockingStatistics) {
        Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
      }
      Handle h_obj(thread, elem->obj());
      assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
             "must be NULL or an object");
      if (UseBiasedLocking) {
        // Retry fast entry if bias is revoked to avoid unnecessary inflation
        ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
      } else {
        ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
      }
      assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
             "must be NULL or an object");
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
    IRT_END
    
    

    上面的代码有个if (UseBiasedLocking)判断,是判断是否使用偏向锁的,本例中的锁显然已经不属于当前线程C了,所以我们还是直接看slow_enter(h_obj, elem->lock(), CHECK)方法吧;

    打开openjdk/hotspot/src/share/vm/runtime/synchronizer.cpp:

    void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
      markOop mark = obj->mark();
      assert(!mark->has_bias_pattern(), "should not see bias pattern here");
    
      //是否处于无锁状态
      if (mark->is_neutral()) {
        // Anticipate successful CAS -- the ST of the displaced mark must
        // be visible <= the ST performed by the CAS.
        lock->set_displaced_header(mark);
        //无锁状态就去竞争锁
        if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
          TEVENT (slow_enter: release stacklock) ;
          return ;
        }
        // Fall through to inflate() ...
      } else
      if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
        //如果处于有锁状态,就检查是不是当前线程持有锁,如果是当前线程持有的,就return,然后就能执行同步代码块中的代码了
        assert(lock != mark->locker(), "must not re-lock the same lock");
        assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
        lock->set_displaced_header(NULL);
        return;
      }
    
    #if 0
      // The following optimization isn't particularly useful.
      if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
        lock->set_displaced_header (NULL) ;
        return ;
      }
    #endif
    
      // The object header will never be displaced to this lock,
      // so it does not matter what the value is, except that it
      // must be non-zero to avoid looking like a re-entrant lock,
      // and must not look locked either.
      lock->set_displaced_header(markOopDesc::unused_mark());
      //锁膨胀
      ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
    }
    
    

    线程C在上面代码中的执行顺序如下:

    1、判断是否是无锁状态,如果是就通过Atomic::cmpxchg_ptr去竞争锁;
    2、不是无锁状态,就检查当前锁是否是线程C持有;
    3、不是线程C持有,调用inflate方法开始锁膨胀;

    ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);

    来看看锁膨胀的源码:
    在这里插入图片描述
    如上图,锁膨胀的代码太长,我们这里只看关键代码吧:
    红框中,如果当前状态已经是重量级锁,就通过mark->monitor()方法取得ObjectMonitor指针再返回;
    绿框中,如果还不是重量级锁,就检查是否处于膨胀中状态(其他线程正在膨胀中),如果是膨胀中,就调用ReadStableMark方法进行等待,ReadStableMark方法执行完毕后再通过continue继续检查,ReadStableMark方法中还会调用os::NakedYield()释放CPU资源;

    如果红框和绿框的条件都没有命中,目前已经是轻量级锁了(不是重量级锁并且不处于锁膨胀状态),可以开始膨胀了,如下图:

    在这里插入图片描述
    简单来说,锁膨胀就是通过CAS将监视器对象OjectMonitor的状态设置为INFLATING,如果CAS失败,就在此循环,再走前一副图中的的红框和绿框中的判断,如果CAS设置成功,会继续设置ObjectMonitor中的header、owner等字段,然后inflate方法返回监视器对象OjectMonitor;

    看看之前slow_enter方法中,调用inflate方法的代码如下:

    ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
    
    

    所以inflate方法返回监视器对象OjectMonitor之后,会立刻执行OjectMonitor的enter方法,这个方法中开始竞争锁了,方法在openjdk/hotspot/src/share/vm/runtime/objectMonitor.cpp文件中:
    在这里插入图片描述
    如上图,红框中表示OjectMonitor的enter方法一进来就通过CAS将OjectMonitor的_owner设置为当前线程,绿框中表示设置成功的逻辑,第一个if表示重入锁的逻辑,第二个if表示第一次设置_owner成功,都意味着竞争锁成功,而我们的线程C显然是竞争失败的,会进入下图中的无线循环,反复调用EnterI方法:
    在这里插入图片描述
    进入EnterI方法看看:
    在这里插入图片描述
    如上图,首先构造一个ObjectWaiter对象node,后面的for(;;)代码块中来是一段非常巧妙的代码,同一时刻可能有多个线程都竞争锁失败走进这个EnterI方法,所以在这个for循环中,用CAS将_cxq地址放入node的_next,也就是把node放到_cxq队列的首位,如果CAS失败,就表示其他线程把node放入到_cxq的首位了,所以通过for循环再放一次,只要成功,此node就一定在最新的_cxq队列的首位。

    接下来的代码又是一个无限循环,如下图:
    在这里插入图片描述
    从上图可以看出,进入循环后先调用TryLock方法竞争一次锁,如果成功了就退出循环,否则就调用Self->_ParkEvent->park方法使线程挂起,这里有自旋锁的逻辑,也就是park方法带了时间参数,就会在挂起一段时间后自动唤醒,如果不是自旋的条件,就一直挂起等待被其他条件唤醒,线程被唤醒后又会执行TryLock方法竞争一次锁,竞争不到继续这个for循环;

    到这里我们已经把线程C在BLOCK的时候的逻辑理清楚了,小结如下:
    1、偏向锁逻辑,未命中;
    2、如果是无锁状态,就通过CAS去竞争锁,此处由于锁已经被线程B持有,所以不是无锁状态;
    3、不是无锁状态,而且锁不是线程C持有,执行锁膨胀,构造OjectMonitor对象;
    4、竞争锁,竞争失败就将线程加入_cxq队列的首位;
    5、开始无限循环,竞争锁成功就退出循环,竞争失败线程挂起,等待被唤醒后继续竞争;

    线程B在notify()的时候做了什么

    接下来该线程B执行notify了,代码是objectMonitor.cpp的ObjectMonitor::notify方法:
    在这里插入图片描述
    如上图所示,首先是Policy的赋值,其次是调用DequeueWaiter()方法将_WaitSet队列的第一个值取出并返回,还记得_WaitSet么?所有wait的线程都被包装成ObjectWaiter对象然后放进来了;
    接下来对ObjectWaiter对象的处理方式,根据Policy的不同而不同:
    Policy == 0:放入_EntryList队列的排头位置;
    Policy == 1:放入_EntryList队列的末尾位置;
    Policy == 2:_EntryList队列为空就放入_EntryList,否则放入_cxq队列的排头位置;

    在这里插入图片描述
    如上图所示,请注意把ObjectWaiter的地址写到_cxq变量的时候要用CAS操作,因为此时可能有其他线程正在竞争锁,竞争失败的时候会将自己包装成ObjectWaiter对象加入到_cxq中;

    Policy == 3:放入_cxq队列中,末尾位置;更新_cxq变量的值的时候,同样要通过CAS注意并发问题;

    这里有一段很巧妙的代码,现将_cxq保存在Tail中,正常情况下将ObjectWaiter赋值给Tail->_next就可以了,但是此时有可能其他线程正在_cxq的尾部追加数据了,所以此时Tail对象对应的记录就不是最后一条了,那么它的_next就非空了,一旦发生这种情况,就执行Tail = Tail->_next,这样就获得了最新的_cxq的尾部数据,如下图所示:
    在这里插入图片描述
    Policy等于其他值,立即唤醒ObjectWaiter对应的线程;

    小结一下,线程B执行notify时候做的事情:

    1、执行过wait的线程都在队列_WaitSet中,此处从_WaitSet中取出第一个;
    2、根据Policy的不同,将这个线程放入_EntryList或者_cxq队列中的起始或末尾位置;

    线程B释放锁的时候做了什么

    接下来到了揭开问题的关键了,我们来看objectMonitor.cpp的ObjectMonitor::exit方法;
    在这里插入图片描述
    如上图,方法一进来先做一些合法性判断,接下来如红框所示,是偏向锁逻辑,偏向次数减一后直接返回,显然线程B在此处不会返回,而是继续往下执行;

    根据QMode的不同,有不同的处理方式:

    1、QMode = 2,并且_cxq非空:取_cxq队列排头位置的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,此处会立即返回,后面的代码不会执行了;
    2、QMode = 3,并且_cxq非空:把_cxq队列首元素放入_EntryList的尾部;
    3、QMode = 4,并且_cxq非空:把_cxq队列首元素放入_EntryList的头部;
    4、QMode = 0,不做什么,继续往下看;
    5、只有QMode=2的时候会提前返回,等于0、3、4的时候都会继续往下执行:

    如果_EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回;
    如果_EntryList的首元素为空,就取_cxq的首元素,放入_EntryList,然后再从_EntryList中取出来执行ExitEpilog方法,然后立即返回;

    以上操作,均是执行过ExitEpilog方法然后立即返回,如果取出的元素为空,就执行循环继续取;

    小结一下,线程B释放了锁之后,执行的操作如下:
    1、偏向锁逻辑,此处未命中;
    2、根据QMode的不同,将ObjectWaiter从_cxq或者_EntryList中取出后唤醒;
    3、唤醒的元素会继续执行挂起前的代码,按照我们之前的分析,线程唤醒后,就会通过CAS去竞争锁,此时由于线程B已经释放了锁,那么此时应该能竞争成功;
    到了现在已经将之前的几个问题搞清了,汇总起来看看:

    线程A在wait() 后被加入了_WaitSet队列中;
    线程C被线程B启动后竞争锁失败,被加入到_cxq队列的首位;
    线程B在notify()时,从_WaitSet中取出第一个,根据Policy的不同,将这个线程放入_EntryList或者_cxq队列中的起始或末尾位置;
    根据QMode的不同,将ObjectWaiter从_cxq或者_EntryList中取出后唤醒;;
    所以,最初的问题已经清楚了,wait()的线程被唤醒后,会进入一个队列,然后JVM会根据Policy和QMode的不同对队列中的ObjectWaiter做不同的处理,被选中的ObjectWaiter会被唤醒,去竞争锁;
    这里Policy=2,表示线程A从等待队列_WaitSet中被取出,又因为_EntryList为空,所以A放入了_EntryList首位,BlOCKING状态的线程C在_cxq,所以A和C放在不同的队列中。其次,QMode=0,在ObjectMonitor::exit方法中,对QMode等于1、2、3、4的时候都有特殊处理(例如从_EntryList中取出数据),但是对QMode等于0没有特殊处理,而是依次从_EntryList中取出线程来唤醒,由于A放在_EntryList中,所以A总是先唤醒;

    在这里插入图片描述

    展开全文
  • 进程的描述与控制 操 作 系 统 所 具 有 的 四 大 特 征 也 都 是 基 于 进 程 而 形 成 的 , ...图中的每个结点可用来表示一个进程或程序段,乃至一条语句,结点间的有向边则表示两个结点之间存在的偏序(Partial Orde
  •  本项目只有两个进程A,B ,大量访问表A 和表B,出现的次数也不是很频繁,所以在进程A中所有访问表A和表B中加了Nolock,在进行读数据时并不锁表,测试还不会会出现上述死锁问题。 ...
  • 【操作系统】第五话·进程的切换与进程控制

    千次阅读 多人点赞 2022-04-02 10:01:30
    从今天开始,我们将要开启一个新的系列【闪耀计划】,没错!这是今年上半年的一整个系列计划!本专题目的是通过百天刷题计划,通过题目和知识点串联的方式,完成对计算机操作系统的复习和巩固;同时还配有专门的笔记...
  • 进程资源图的识读、判断节点是否阻塞、化简

    千次阅读 多人点赞 2020-10-31 16:02:51
    让我们来看一下这道13年上半年的软设选择题:   先po答案:空1填B、空2填C ...(3)判断一个进程节点是否阻塞? 读图时,先看资源分配R→P,再看资源申请P→R 【注意】 读图时,**不要**将同时存在R→P
  • 同步就是一个任务的完成需要依赖另外一个任务时,只有等待依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。 异步是不需要等待依赖...
  • 笔记——MPI的阻塞通信

    千次阅读 2021-12-07 21:43:35
    阻塞式发送可以理解为,只有进程在确定消息完全发出去(发送到缓冲区)后才会继续执行下条指令,在此之前会一直等待;阻塞式接受也可以理解为,当前进程在未接受到完整的消息时,会一直等待,直到接收结束才会继续...
  • 操作系统之进程管理习题

    千次阅读 2021-12-11 22:19:54
    操作系统进程管理习题详解
  • 操作系统精选习题——第二章

    千次阅读 2021-07-05 14:59:32
    一个进程被唤醒意味着( )。 A、该进程重新占有了CPU B、它的优先权变为最大 C、其PCB移至阻塞队列队首 D、进程变为就绪状态 假设某时刻,若干进程调用了P(S)后,有n个进程处于等待信号量S的状态。此后,又有m个进程...
  • 进程(Process)

    千次阅读 2021-11-02 18:55:19
    就是说,一个程序加载到内存后就变为进程。即:进程=程序+执行 单一操作员单一控制终端、批处理均存在效率低下的问题,即CPU使用率不高。为了提高CPU利用率,人们想起将多个程序同时加载到计算机里,并发执行。这些...
  • 互斥######################### 中断屏蔽*************************...因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法(换句话说,驱动中使用local_irq_disable/enable()通常意味着一个bug),它适...
  • 像突如其来温暖我的春天,像倾盆大雨我躲避的屋檐,爱不会磨灭,逝去的人住在心间 ~
  • 多个进程可同时对同一个文件作共享锁定。 LOCK_EX 建立互斥锁定。一个文件同时只有一个互斥锁定。 LOCK_UN 解除文件锁定状态。 LOCK_NB 无法建立锁定时,此操作可不阻断,马上返回进程。通常与LOCK_SH或LOCK_EX 做...
  • Linux进程阻塞的相关知识

    千次阅读 2013-08-18 20:26:24
    1.如果驱动程序无法立即满足要求,该如何响应? 当数据不可用时,用户可能调用read;或者进程试图写入数据,但因为输出缓冲区已满,设备还未准备好接受数据。...2.“休眠(sleep)”对进程来讲意味着什么
  • . 单选题(共38题,38分) 1.(单选题)速度最快的进程通信方式是() ...3.(单选题)正在执行的进程由于其时间片用完而暂停运行,此时该进程应从运行态变为()。 A. 运行态 B. 等待态 C. 就绪态 D. 终止态 ...
  • 进程线程与IO

    千次阅读 2022-03-11 11:45:16
    如果转换到阻塞态表明,进程进行某种行为被阻塞了,通常是系统调用下的IO操作,需要切换下一个进程使用CPU资源。 调度时操作系统的行为: 进程切换时会进行程序中断,操作系统进入管态(见3.1),操作系统记录当前...
  • C语言进程——基础

    千次阅读 2022-04-21 08:59:05
    什么是进程 什么是程序 一组可以计算机直接识别的 有序 指令 的集合。 通俗讲:C语言编译后生成的...一个程序可以只有一个进程,此时正在运行的这个程序也叫进程一个程序也可以有多个进程,此时正在运行的这个程序
  • socket阻塞与非阻塞,同步与异步

    万次阅读 2020-06-18 14:42:52
    一个概念:用户空间与内核空间 1. 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方) 2. 操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的...
  • 一、I/O 模型 阻塞式 I/O 非阻塞式 I/O I/O 复用 信号驱动 I/O 异步 I/O 五大 I/O 模型比较 二、I/O 复用 ...对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据...
  • 在Linux中,我们通常使用fork函数来为一个已经存在的进程创建一个进程。而这个新创建出来的进程被称为原进程的子进程,原进程被称为该进程的父进程。 该函数其实是一个系统调用接口,原型如下: #include <...
  • 每个APP可能运行在一个进程,也可能在多个进程,这些进程拥有自己独立的资源;然而这些进程都是由Zygote进程fork出来的,再往前一步其实是system server进程使用LocalSocket去通知zygote进程,然后zygote去fork一...
  • 一个线程是无法获取到CPU执行权的(调用sleep方法是进入到睡眠暂停状态,但是CPU执行权并没有交出去,而调用wait方法则是将CPU执行权交给另一个线程),这个时候就会造成线程阻塞。 为什么会出现线程阻塞? 1....
  • [ZUCC 操作系统]进程概念

    千次阅读 2020-03-25 13:10:10
    一个被创建的进程包括( )。 A. PCB B. 程序和数据 C. PCB和数据 D. PCB、程序和数据 问题 2 下面对进程的描述中,错误的是( )。 A. 进程是动态的概念 B. 进程有生命期 C. 进程可并发执行 D. 进程是指令的集合 问题 3...
  • 本文中的 fork 只会介绍基本使用,以及解答 fork 为啥会有 2 返回值、为啥给子进程返回 0,而父进程返回子进程的 pid;而对于用于接收 fork 返回值的 ret 是怎么做到 ret == 0 && ret > 0、写时拷贝、代码是怎么...
  • 意味着在远程www.javathinker.org主机的80端口上,监听到了一个客户的连接请求。管理客户连接请求的任务是由操作系统来完成的。操作系统把这些连接请求存储在一个先进先出的队列中。许多操作系统限定了队列的最大...
  • 操作系统大纲: 进程和线程知识体系: 1. 进程 在多程序系统中,操作系统调度CPU上的进程以获得最大的利用率,此... 短期调度程序从就绪队列中选择另一个进程并将CPU分配给此进程。 这个过程称为上下文切换。
  • 进程调度

    千次阅读 2012-12-04 15:27:35
    这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。 目录 进程有四个基本属性进程的三种基本状态处理机调度的分级进程调度的方式 非剥夺方式...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 97,583
精华内容 39,033
关键字:

一个进程被阻塞意味着