Java多线程之中断、终止、休眠、让步
文章目录
一、概述
一个线程的生命历程总是丰富多彩的,时常伴随着中断、休眠、让步甚至是最后的终止。接下来就让我们深入了解一下线程的生命历程吧!
二、中断(唯标识位)
1、定义
中断可以理解为线程的一个标识位,它表示一个运行中的线程是否被其它线程进行了中断操作。中断好比其它线程对它打了个招呼,其它线程通过调用该线程的interrupt()方法对其进行中断操作。
线程之中,一个线程的生命周期分为:初始、就绪、运行、阻塞以及结束。当然,其中也可以有四种状态,初始、就绪、运行以及结束。
一般而言,可能有三种原因引起阻塞:等待阻塞、同步阻塞以及其他阻塞(睡眠、join或者IO阻塞);对于Java而言,等待阻塞是调用wait方法产生的,同步阻塞则是由同步块(synchronized)产生的,睡眠阻塞是由sleep产生的,join阻塞是由join方法产生的。
api | 含义 |
---|---|
public void interrupt() | 中断线程,中断并不是真正的中断线程,而只设置标志位(中断位)来通知用户 |
public boolean isInterrupt() | 判断Thread 对象是否中断 |
public static boolean interrupted | 判断 当前线程是否中断,并且清除当前中断状态 |
2、操作过程
线程通过检查自身是否被中断来进行响应,线程通过isInterrupted()来进行判断是否被中断,也可以调用Thread.interrupt()来对当前线程的中断标识位进行复位。
-
线程中断自己是被允许的;其它线程对本线程进行中断,会检测权限,checkAccess(),如果不允许,则会抛出SecurityException异常。
-
言归正传,要中断一个Java线程,可调用线程类(Thread)对象的实例方法:interrupt();然而interrupt()方法并不会立即执行中断操作;底层实现是,这个方法只会给线程设置一个为true的中断标志(中断标志只是一个布尔类型的变量),而设置之后,则根据线程当前的状态进行不同的后续操作。如果,线程的当前状态处于非阻塞状态,那么仅仅是线程的中断标志被修改为true而已;如果线程的当前状态处于阻塞状态,那么在将中断标志设置为true后,还会有如下三种情况之一的操作:
- 如果是wait、sleep以及join三个方法引起的阻塞,那么会将线程的中断标志重新设置为false,并抛出一个InterruptedException
- 如果是java.nio.channels.InterruptibleChannel进行的io操作引起的阻塞,则会对线程抛出一个ClosedByInterruptedException
- 如果是轮询(java.nio.channels.Selectors)引起的线程阻塞,则立即返回,不会抛出异常
-
如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理;例如,一个线程在运行状态中,其中断标志被设置为true,则此后,一旦线程调用了wait、join、sleep方法中的一种,立马抛出一个InterruptedException,且中断标志被清除,重新设置为false。
通过上面的分析,我们可以总结,调用线程类的interrupt方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常**。因此,通过interrupt方法真正实现线程的中断原理是:**开发人员根据中断标志的具体值,来决定如何退出线程.
3、原理
以下利用一个demo来讲解中断的原理
public class ThreadSuspendTest {
static class ThreadOne extends Thread{
public void run(){
try {
for (int i=0; i < 5000; i++){
if (this.isInterrupted()){
//判断是否是中断状态,如果是,就抛出异常,不执行下面的任务
System.out.println("已经是停止状态了,我要退出");
throw new InterruptedException();
}
//非中断情况下执行的任务
System.out.println("i = " + (i+1));
}
}catch (InterruptedException e){
//中断情况下执行的任务
System.out.println("进入InterruptThread的catch块");
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread t= new ThreadOne();
t.start();
//对线程进行中断
t.interrupt();
//中断主线程
Thread.currentThread().interrupt();
//判断当前线程是否中断,并清除当前线程的中断状态true->false
System.out.println("main:" + Thread.currentThread().getName() + "是否停止1? = " + Thread.interrupted());
//因此这里会返回false
System.out.println("main:" + Thread.currentThread().getName() + "是否停止2? = " + Thread.interrupted());
//而isInterrupted()是返回线程对象是否处于中断状态。
System.out.println(t.getName() + "是否停止3? = " + t.isInterrupted());
System.out.println(t.getName() + "是否停止4? = " + t.isInterrupted());
}
}
对上面的代码进行分析:
- 线程中断并非真正意义上的对线程进行中断操作,而是改变标识位。
- 如上,使用 t.interrupt() 对t线程进行中断操作,那么,意味着t线程的中断标识位为true,这个时候,t线程内部的run方法检测到中断标识位,就抛出中断异常,执行catch块的代码,而不执行原先的非中断情况下的代码,从而达到一种由外部操作控制线程内部的运行过程的目的,这就是线程中断的真正目的。
源码层面的分析:
- interrupt
public void interrupt() {
//如果不是本身线程,需要判断是否具有设置中断标识权限
if (this != Thread.currentThread())
//如果没有权限,会抛出异常
checkAccess();
//持有锁
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
//从JNI层面去设置标志位
interrupt0(); // Just to set the interrupt flag 仅仅只是设置标志位
b.interrupt(this);
return;
}
}
interrupt0();
}
- interrupted
//返回“当前线程”的中断状态并且对中断状态进行重置。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
- isInterrupted
//返回的是当前线程对象的中断状态,但是不重置中断状态。
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* 测试某个线程是否被中断。中断状态
* 是重置还是不重置取决于clearinterrupt的值
* 通过。
*/
@HotSpotIntrinsicCandidate
private native boolean isInterrupted(boolean ClearInterrupted);
4、中断总结
- 线程中断并非真正意义上的对线程进行中断操作,而是改变标识位。
- 中断标识位的设置是在JVM层面进行设置的,在源码内部调用的是JNI方法。
- interrupt用于设置中断标识位为true;interrupted返回当前运行线程的中断状态并且重置中断状态;isInterrupted返回线程对象的中断状态但是不进行重置。
- 对于sleep、join、wait等方法造成的中断,
三、休眠
1、定义
sleep() 的作用是让当前线程休眠,即当前线程会从“[运行状态]”进入到“[休眠(阻塞)状态]”。sleep()会指定休眠时间,线程休眠的时间会大于/等于该休眠时间;在线程重新被唤醒时,它会由“[阻塞状态]”变成“[就绪状态]”,从而等待cpu的调度执行。
执行的流程如下:
2、作用
-
主动让出CPU执行占有权,特别是在利用死循环进行状态的检测、监听的时候,适当的休眠降低CPU的使用率。
-
线程池的应用,在线程池中,对空闲的线程进行休眠,而当有任务到来,就执行唤醒线程,执行对应的任务。达到线程复用的目的,而不是创建一大堆线程去完成一个个的任务而后销毁。
-
下面结合线程中断、线程休眠做一个任务调度的小demo,也是线程池的简化原型
public class Worker extends Thread { /** * 需要拆解的字符 */ private String needToSplit; public void setNeedToSplit(String needToSplit) { this.needToSplit = needToSplit; } @Override public void run() { while (true) { try { //当没有需要拆解的字符串时候执行休眠 sleep(5000L); } catch (InterruptedException e) { //如果检测到中断,则唤醒自我,并且执行工作。 if (needToSplit != null) { System.out.println("我唤醒了,执行工作..." + needToSplit); //执行完毕,提交任务 needToSplit = null; } } } } }
public class Dispatcher { private static final List<Worker> workers = new LinkedList<>(); public static void main(String[] args) { //招募三个工作线程 for (int i = 1; i < 4; i++) { workers.add(new Worker()); //同时启动三个工作线程 workers.get(i-1).start(); } TaskManager tasks = new TaskManager(); List<String> taskList = new LinkedList<>(); taskList.add("我是任务1"); taskList.add("我是任务2"); taskList.add("我是任务3"); tasks.setStrings(taskList); for (String task : tasks.getStrings() ) { workers.get(0).setNeedToSplit(task); //当有任务来了,就中断休眠,执行任务 workers.get(0).interrupt(); } } }
public class TaskManager { private List<String> strings; public List<String> getStrings() { return strings; } public void setStrings(List<String> strings) { this.strings = strings; } }
3、原理
讲原理,先上源码:
public static native void sleep(long millis) throws InterruptedException;
哈哈!就是直接调用JNI,由JVM去负责调度休眠。
四、终止
1、遭到弃用的三大将
- suspend
- 当前线程挂起,占有资源进入一种睡眠状态,容易造成死锁问题
- resume
- resume本身就是对suspend的线程进行恢复,弃用了suspend,自然也就没有它什么事情了
- stop
- stop强制终止线程,最大的问题是没有预留时间给线程完成资源的释放工作。
五、让步
1、定义
当前线程作出让步,把cpu执行权交给join方法的调用线程对象。
2、作用
- 可以实现分流任务的处理
- 可以实现缓存的准确清理
3、实现原理
直接上源码:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
//直到分线程执行完毕
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
//特定时间的等待
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}