多线程与高并发(四)—— 根据 HotSpot 源码讲透 Java 中断机制

前言

我们首先介绍中断的三个 APPI 及其底层代码,在对方法的实现有了清晰的认知后,再结合场景谈谈什么是中断,以及中断该如何正确使用?

一、中断方法

1. isInterrupted

public boolean isInterrupted() {
      // 调用isInterrupted 方法,中断标记设置为 true
        return isInterrupted(false);
    }

这个方法很简单,就是返回当前线程的中断标记值,这个方法是由 native 方法实现。

/**
     * Tests if some Thread has been interrupted.  The interrupted state
     * is reset or not based on the value of ClearInterrupted that is
     * passed.
     */
    private native boolean isInterrupted(boolean ClearInterrupted);

根据 Thread 类找到对应路径下 Thread.c,可以看到该方法的底层实现方法JVM_IsInterrupted:

在 hotspot 源码 jvm.c 文件中,可以看到 JVM_IsInterrupted 的底层实现依赖于操作系统的 is_interrupted 方法,我们就看 os_linux 的实现:

该方法中的逻辑也很简单,就是返回了操作系统的线程中断状态,如果清除标记为 true,那么就重置中断标记

bool os::is_interrupted(Thread* thread, bool clear_interrupted) {
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

// 拿到对应的操作系统线程
  OSThread* osthread = thread->osthread();

// 获取该线程的中断状态
  bool interrupted = osthread->interrupted();

// 如果中断状态为true 并且需要清除中断标记,那么将中断标记重置为 false
  if (interrupted && clear_interrupted) {
    osthread->set_interrupted(false);
    // consider thread->_SleepEvent->reset() ... optional optimization
  }
// 返回中断标记
  return interrupted;
}

2. interrupted()

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

看到这个方法可以发现,它和 isinterrupted() 的实现都是调用 isInterrupted 方法,只是参数不一样,interrupted 的参数为 true,这个参数的含义就是是否要清除中断标记。因此该方法的作用是返回当前线程的中断标记并且重置中断标记。

3. interrupt()

public void interrupt() {
        // 检查调用方线程是否有对被调用线程的修改权
        if (this != Thread.currentThread())
            checkAccess();

        // 中断网络 IO
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        // 调用 native 方法
        interrupt0();
    }

先忽略中断网络 IO 这块的代码,下面会讲,可以看到这个方法最终依赖 native 方法 interrupt0(),关注其底层实现,可以发现该中断方法主要做了两件事情:

  • 设置中断标记
  • 唤醒当前线程
void os::interrupt(Thread* thread) {
    // 确保执行该方法的线程是当前线程
  assert(Thread::current() == thread || Threads_lock->owned_by_self(),
    "possibility of dangling Thread pointer");

  OSThread* osthread = thread->osthread();

  if (!osthread->interrupted()) {
      // 设置中断标记
    osthread->set_interrupted(true);
    // More than one thread can get here with the same value of osthread,
    // resulting in multiple notifications.  We do, however, want the store
    // to interrupted() to be visible to other threads before we execute unpark().
      // 屏障指令,保证在执行 unpark() 之前对其他线程可见
    OrderAccess::fence();
    // 唤醒 sleep() 阻塞的线程
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

    // 唤醒 park() 阻塞的线程
  // For JSR166. Unpark even if interrupt status already was set 只有中断状态被设置了之后才能执行 unpark
  if (thread->is_Java_thread())
    ((JavaThread*)thread)->parker()->unpark();

    // 线程被wait()方法阻塞的线程
  ParkEvent * ev = thread->_ParkEvent ;
  if (ev != NULL) ev->unpark() ;

}

根据上述源码可以发现 Java 中的中断并没有中断任务的功能,所提供的仅仅是设置标记以及唤醒线程,对应的也就是下述两种应用场景,如要中断任务需要开发者根据打断标记自行编码打断任务,如要阻止线程继续阻塞,则唤醒线程。

注意该方法上的注释(中文翻译),在执行 interrupt() 方法时,其产生的结果因不同场景而不同,具体如下:

  • 线程因调用 object.wait、Thread.join、Thread.sleep 方法阻塞,将会抛出InterruptedException,同时清除线程的中断状态;
  • 线程阻塞在 java.nio.channels.InterruptibleChannel 的 IO 上,Channel 将会被关闭,线程被置为中断状态,并抛出 java.nio.channels.ClosedByInterruptException;
  • 如果线程堵塞在 java.nio.channels.Selector 上,线程被置为中断状态,select方法会马上返回,类似调用wakeup的效果;
  • 如果线程处于 not alive(线程刚被 new 出来没有运行,或者已经死亡) 的状态则毫无影响

二、什么是中断?

中断,顾名思义是对线程的当前状态的改变,使其恢复到原有状态,具体如何改变状态依据不同的情景有着不同的结果。在对 Java 的中断能不能真正的中断线程这个问题上,不少博客各执一词,最后我发现大家是在鸡同鸭讲。能否中断线程因线程状态而异,具体如下:

中断任务的执行

在某些时候,如生产者/消费者模式下,线程循环执行任务,如何让线程停下来?
在 Java 中没有提供停止线程的操作(stop()方法会导致错误,已被官方标记过时),Java 提供 interrupt() 方法设置中断标记,但是如何停止线程却是开发者自己决定的事情。
上面源码讲述已经提到 interrupt() 方法并没有提供打断线程的机制(如下图,线程不会停止运行),要实现运行中的线程中断,需要调用 interrupt() 修改线程的中断标记,然后需要在被调用线程中自己实现逻辑,通常的做法是在合适的位置(例如在while 循环处)不断检查此线程的中断标记是否被设置,在检测到中断标记为 true 的地方再停止线程(使用方式可搜索两阶段终止提交,此文不涉及应用)。

中断线程的阻塞状态

如下图是中断线程的阻塞状态,在线程检查到中断标记为true 的时候会在这些阻塞方法调用处抛出 InterruptedException , 并且清除中断标记。

并不是所有阻塞态的线程都能被中断,Synchronized 不支持锁中断,见下文。

三、其他

关于 Synchronized 不可中断

先看以下场景,T2 和 T1 竞争锁,在 T1 因获取锁阻塞的时候去打断,看结果如何。

/**
 * description
 *
 * @author greysonchance
 * @since 2022/8/1
 */
@Slf4j
public class TestSynchronized {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                log.info("线程 T1 拿到锁, 开始执行");
                while (!Thread.currentThread().isInterrupted()) {

                }
            }
            log.info("线程 T1 退出");
        }, "T1");

        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.info("线程 T2 拿到锁, 开始执行");
                t1.start();
                try {
                    // 保证 T1 线程已经开始获取锁进入阻塞态
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log.info("T2 输出线程 T1 的状态是:{}", t1.getState());

                // 死循环不释放锁,让 T1 一直处于 Blocked 态
                while (true) {

                }
            }
        }, "T2");

        t2.start();
        log.info("开始中断线程 T1");
        t1.interrupt();
        try {
            // 等待一会看状态是否被修改
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("Main 输出线程 T1 的状态是:{}", t1.getState());

    }

}

如图,线程 T2 启动并且先获取到锁, 然后 t1.start() 启动 T1 线程,但是 T1 因获取不到锁阻塞,此时执行 interrupt 方法并不能中断 T1,线程仍然处于 Blocked 态。

这是因为线程 T1 因获取不到锁,阻塞在 Monitor 的队列中,interrupt() 方法并不能将该线程从队列中移出。

再看下面场景,线程获取锁之后调用了 wait() 方法,然后再调用 interrupt() 方法,线程被中断成功

/**
 * description
 *
 * @author greysonchance
 * @since 2022/8/1
 */
@Slf4j
public class TestSynchronized2 {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                log.info("线程 T1 拿到锁, 开始执行");
                for (int i = 0; i < 3; i++) {
                    System.out.println("执行任务");
                }
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            log.info("线程 T1 退出");
        }, "T1");

        t1.start();
        Thread.sleep(1000);
        t1.interrupt();
        Thread.sleep(1000);
        log.info("线程 T1 状态:{}", t1.getState());
    }
}

综上,Synchronized 的不可中断指的是当线程因获取锁失败阻塞在队列中时(状态为 BLOCKED)不可被打断,如果已经获取了锁调用 sleep()\wait() 等方法而阻塞(状态为 WAITED或TIME_WAITING)是可以被打断的。

interrupt() 与 park() 对 unpark() 的影响

interrupt() 在唤醒被 unpark() 阻塞的线程时会修改中断标记为true,而 park() 唤醒不会。

@Slf4j
public class TestUnpark {
    public static void main(String[] args) {
        Thread t0 = new Thread(new Runnable() {
            @Override
            public void run() {
                Thread current = Thread.currentThread();
                log.info("current Thread name:{}", current.getName());
                log.info("准备park当前线程:{}", current.getName());
                log.info("当前线程的中断标记:{}",current.isInterrupted());
                LockSupport.park();
                log.info("线程{}阻断后又运行了", current.getName());
                log.info("当前线程被唤醒后的中断标记:{}",current.isInterrupted());

            }
        }, "t0");
        t0.start();
        try {
            log.info("休眠…………");
            Thread.sleep(2000);
//            log.info("调用LockSupport.unpark方法,唤醒线程{}", t0.getName());
//            LockSupport.unpark(t0);
            log.info("调用interrupt方法,唤醒线程{}", t0.getName());
            t0.interrupt();
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

interrupt() 的实验结果:

unpark() 的实验结果:

posted @ 2022-08-02 10:58  onAcorner  阅读(95)  评论(0编辑  收藏  举报