线程与进程的阻塞
线程阻塞
线程在运行的过程中因为某些原因而发生阻塞,阻塞状态的线程的特点是:该线程放弃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指令时会