线程运行的基本原理

在​​java​​应用程序中,使用​​new Thread().start()​​来启动一个线程时,底层会进行怎样的处理?我们通过一个简单的流程图来进一步分析:

聊一聊线程是如何运行的_i++

如上图,​​java​​代码中创建并启动了一个线程,在​​JVM​​中调用相关方法通知到操作系统,操作系统首先接收到​​JVM​​指令会先创建一个线程出来,这时候线程并不会马上执行,它会通过操作系统​​CPU​​调度算法把该线程分配给某个​​CPU​​来执行,​​CPU​​执行任务的时候就会回调线程中的​​run()​​方法来执行相关指令。

线程的运行状态

一个线程从启动到销毁的这一个生命周期中会经历各种不同的状态,微观上​​java​​应用中线程一共分为6种状态:

  • NEW:新建状态,当执行​​new Thread()​​的时候线程处于此状态。
  • RUNNABLE:运行状态,线程调用​​start()​​方法启动线程后的状态,一般线程调用​​start()​​后会进入一个队列就绪,等获得CPU执行权后才真正开始执行线程中的​​run()​​代码块。
  • BLOCKED:阻塞状态,当线程在执行​​synchronized​​代码块,没有抢占到同步锁时会变成阻塞状态。
  • WAITING:等待状态,当调用​​Object.wait()​​方法时,线程会进入该等待状态。
  • TIMED_WAITING:超时等待状态,例如​​Thread.sheep(timeout)​​超时后会自动唤醒线程。
  • TERMINATED:终止状态,当线程中的​​run()​​方法正常执行完或者调用​​interrupt()​​的时候线程变为此状态。

从宏观上看就分为五种状态:新建、就绪、运行、等待、死亡,整体的状态运行流转如下图:

聊一聊线程是如何运行的_阻塞状态_02

如何终止线程

首先​​run()​​方法中的指令正常运行结束后线程自然会进入终止状态。那么如果我们想要终止一个运行中的线程该怎么办?

使用stop()终止

使用stop()​方法,该方式肯定是行不通的,该方法会强制停止一个线程的执行,并且会释放线程中所占用的锁,这种锁的释放是不可控的。

static class StopThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 100000; i++) {
System.out.println("count:" + i);
}
System.out.println("thread run finish!");
}
}

public static void main(String[] args) throws InterruptedException {
StopThread stopThread = new StopThread();
stopThread.start();
Thread.sleep(50);
stopThread.stop();
}

由以上代码所展示的在​​for​​循环未结束时就提前终止线程,导致最后的​​System.out.println("thread run finish!");​​不会正常执行结束。

public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}

进入​​println​​的源码看一下,我们就能够发现有​​print(x)、newLint()​​两个操作是原子性的,所以增加了​​synchronized​​同步锁进行保护,按正常是不应该出现问题的,但是执行​​stop()​​操作会强制释放所有锁,从而导致​​println()​​操作的原子性被破坏(上面的代码多运行几次就可能出现最后一次循环没有换行,就是存在newLine()​未被执行的可能),所以实际开发过程中是一定不能使用stop()​来中断线程的

聊一聊线程是如何运行的_阻塞状态_03

聊一聊线程是如何运行的_i++_04

使用interrupt()终止

​Thread​​类也提供了一个方法​​interrupt()​​,从单词意义上看就是中断的意思,但实际操作上并不像​​stop()​​那样直接了断,而是通过一个信号量的方式来通知线程中断的。那么这种情况也是需要有线程自己来觉得是否终止,但是要想让线程安全中断就需要做两件事:

  • 外部线程需要发送一个中断信号给正在运行中的线程。
  • 正常运行中的线程需要根据该信号来判断是否终止线程。

根据以上的条件,我们用一个简单的例子,通过​​interrupt()​​方法进行信号传递,具体代码如下:

static class InterruptThread extends Thread {
@Override
public void run() {
int i = 0;
while (!this.isInterrupted()) {
i++;
}
System.out.println("thread interrupt in:" + i);
}
}

public static void main(String[] args) throws InterruptedException {
InterruptThread interruptThread = new InterruptThread();
interruptThread.start();
TimeUnit.MILLISECONDS.sleep(50);
System.out.println("interrupt status is:" + interruptThread.isInterrupted());
interruptThread.interrupt();
System.out.println("interrupt status is:" + interruptThread.isInterrupted());
}

聊一聊线程是如何运行的_ide_05

上述代码中,首先创建并开启一个线程​​InterruptThread​​,该线程​​run()​​方法中使用​​while​​循环进行计数,判断条件为​​!this.isInterrupted()​​当前线程是否为中断状态,如果线程调用​​interrupt()​​方法那么​​isInterrupted()=true​​,​​while​​条件不通过就停止循环打印控制台日志,线程运行结束。

从这个示例可以看出线程在调用​​interrupt()​​方法后并没有直接了断的把线程中断掉,而是通过传递消息的形式来决定是否停止线程,这样就可以在收到中断信号后继续把​​run()​​方法后面的代码指令执行完,最终达成线程安全中断的目的。

如何中断阻塞状态的线程

如果一个线程处于阻塞状态,那么能否也通过​​interrupt()​​方法进行中断?答案肯定是可以的,具体要怎么操作我们还是先上代码分析:

static class BlockedInterruptThread extends Thread {
@Override
public void run() {
int i = 0;
while (!this.isInterrupted()) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
System.out.println("thread interrupt in:" + i);
}
}

public static void main(String[] args) throws InterruptedException {
BlockedInterruptThread blockedInterruptThread = new BlockedInterruptThread();
blockedInterruptThread.start();
TimeUnit.MILLISECONDS.sleep(50);
System.out.println("interrupt status is:" + blockedInterruptThread.isInterrupted());
blockedInterruptThread.interrupt();
System.out.println("interrupt status is:" + blockedInterruptThread.isInterrupted());
}

聊一聊线程是如何运行的_阻塞状态_06

上面这段代码看是使用​​interrupt()​​通知线程进行中断,但是运行结果我们会发现其实线程并没有被中断,而是打印出了异常堆栈信息并且还在运行中。

为什么我们发出​​interrupt()​​指令而为什么线程没有被中断呢?根据上面我们描述的状态流转图可以看到,线程的状态是不可能从直接状态直接终止的,而是处于阻塞状态的线程必须也只能先进入就绪状态,再进入运行状态之后才能正常终止。所以上面的代码抛出的​​InterruptedException​​异常就是因为线程处于阻塞中被提前唤醒了,也就是说在休眠阻塞时间未结束提前唤醒线程进入了就绪状态。

因此在抛出​​InterruptedException​​异常后就说明当前线程已经被唤醒正常运行了,这时候仍然要中断的话,那么就只需在​​catch​​代码块中再次对当前线程发起一次中断信号​​interrupt()​​即可,代码修改如下:

static class BlockedInterruptThread extends Thread {
@Override
public void run() {
int i = 0;
while (!this.isInterrupted()) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
// 再次发起中断
this.interrupt();
}
i++;
}
System.out.println("thread interrupt in:" + i);
}
}

public static void main(String[] args) throws InterruptedException {
BlockedInterruptThread blockedInterruptThread = new BlockedInterruptThread();
blockedInterruptThread.start();
TimeUnit.MILLISECONDS.sleep(50);
System.out.println("interrupt status is:" + blockedInterruptThread.isInterrupted());
blockedInterruptThread.interrupt();
System.out.println("interrupt status is:" + blockedInterruptThread.isInterrupted());
}

所以说当一个阻塞任务抛出​​InterruptedException​​异常时,并不是意味着线程要终止,而是提醒当前线程有中断操作发生,捕获该异常后要怎么处理,是否继续中断可由线程本身进行把控。比如:

  • 直接捕获异常输出日志不做任何处理,线程继续运行。
  • 将异常抛出让调用方处理。
  • 打印异常信息并停止当前线程。
  • 记录日志,结合数据库或其他中间件做任务重试处理。

总结

理清线程整个生命周期中状态的变化过程,对于多线程环境出现的问题我们就能够快速的去定位分析并解决问题,特别是阻塞中的线程被提前中断要如何处理,阻塞状态的线程必须被唤醒才会继续下一步操作,这就很容易理解为什么要在捕获​​InterruptedException​​异常后再次发起中断信号。