线程与进程的阻塞

线程阻塞
线程在运行的过程中因为某些原因而发生阻塞,阻塞状态的线程的特点是:该线程放弃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 ASSERTthread->last_frame().interpreter_frame_verify_monitor(elem);
#endifif (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 inflationObjectSynchronizer::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 ASSERTthread->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() ...} elseif (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总是先唤醒;

线程和进程/阻塞和挂起以及那些sleep,wait()和notify()方法详解相关推荐

  1. java线程方法notify,JAVA-线程wait方法与notify方法详解

    一.notify的含义(视为使当前线程获取对象锁并于规定一定条件下释放的方法) (1)notify一次只随机通知一个线程进行唤醒(notifyAll则为全部) (2)  在执行了notify方法之后, ...

  2. 线程和进程/阻塞和挂起

    曾多次迷惑于阻塞和挂起状态,后来才发现,有一些文章没有区别,把(阻塞.挂起.等待)等同了,这时看语境作者说的是哪个.自己加以分析区别. 先大概这样理解一下: 挂起:一般是主动的,由系统或程序发出,甚至 ...

  3. java并发锁获取的方式_Java精通并发-notify方法详解及线程获取锁的方式分析

    wait(): 在上一次https://www.cnblogs.com/webor2006/p/11404521.html中对于无参数的wait()方法的javadoc进行了解读,而它是调用了一个参数 ...

  4. Python|线程和进程|阻塞|非阻塞|同步|异步|生成器和协程|资源竞争|进程间通信|aiohttp库|daemon属性值详解|语言基础50课:学习(11)

    文章目录 系列目录 原项目地址 第34课:Python中的并发编程-1 线程和进程 多线程编程 使用 Thread 类创建线程对象 继承 Thread 类自定义线程 使用线程池 守护线程 资源竞争 G ...

  5. Qt中线程同步的几种方法详解

    1.QMutex类 QMutex类就像一把锁,在互斥量以前上锁(QMutex::lock()),而后在使用完互斥量以后解锁(QMutex::unlock()).好比下面的代码:函数 void some ...

  6. python process 函数_Python Process创建进程的2种方法详解

    前面介绍了使用 os.fork() 函数实现多进程编程,该方法最明显的缺陷就是不适用于 Windows 系统.本节将介绍一种支持 Python 在 Windows 平台上创建新进程的方法. Pytho ...

  7. 线程池invokeAll方法详解

    线程池invokeAll方法详解 问题起源与抽象 问题排查与猜测 猜测一:invokeAll 在异步执行后会不会同步等待线程执行完毕获取最终结果 猜测二:队列里面可能存在第一次调用 invokeAll ...

  8. 饥荒怎么自动订阅服务器,饥荒联机版自动挂礼物mod及使用方法详解

    饥荒联机版中官方经常会推出一些挂机掉落礼物的活动,可能一些玩家会觉得很麻烦,下面给大家分享一些自动挂礼物mod和其使用方法,希望可以帮助到各位玩家. 饥荒联机版自动挂礼物mod及使用方法详解 挂礼物m ...

  9. kill -9 进程号杀不死可以用下面的杀掉 Linux下强制杀死进程的方法详解

    常规篇: 首先,用ps查看进程,方法如下: $ ps -ef -- smx 1822 1 0 11:38 ? 00:00:49 gnome-terminal smx 1823 1822 0 11:38 ...

最新文章

  1. \multirow 表格文字居中(latex强制换行)
  2. js遍历追加html子样式,前端基本功:JS(十一)动画封装(CSS样式获取、JSON遍历)...
  3. 【面试题】使用 HashMap 还是 TreeMap
  4. mybatis insert 返回主键_面试准备季——MyBatis 面试专题(含答案)
  5. MySql索引原理与使用大全
  6. Linux下如何杀死终端
  7. ESP32开发板开源啦 ESP32-IOT-KIT全开源物联网开发板
  8. java关系操作符==和equals()区别
  9. 前端js使用java变量值_web前端:js中的变量
  10. Ubuntu20.10 安装RedNotebook,一款不错的日记本
  11. OPPO A11解账号锁刷机包
  12. 动态 Probit 模型及 Stata 实现
  13. “pip-script.py”is not present的问题
  14. Office从2019版本降至2016版本
  15. html调用短信接口发送消息的实例,HTTP电脑发送短信接口调用示例
  16. 打字游戏之主界面实现
  17. JavaSE基础(134) 打印流
  18. java计算机毕业设计学校食堂订餐管理源码+数据库+系统+lw文档+部署
  19. Unity:看不到Flash未来 终止支持Flash
  20. 六度短网址服务平台原理

热门文章

  1. CSDN初体验,尝试完成一个自动扫雷程序
  2. 你有颜值我有故事 毕业季跟“自拍神器”来一场疯狂约会
  3. linux内核驱动ldd3_《Linux设备驱动程序》编译LDD3的scull驱动问题总结***
  4. US Domain Center VPS 主机
  5. P3243 [HNOI2015]菜肴制作
  6. IMU误差模型与校准
  7. 命令行中 python -v 和 python -V 详解
  8. 用了几年的postman,原来只用了皮毛~
  9. 计算机单位、变量、数据类型、类型转换、转义字符
  10. linux分辨率无法调,centos root用户分辨率无法调整问题