• 一文搞懂Java线程中断协商机制,如何优雅中断一个正在运行的线程?


    一、中断机制概述

    1、中断API

    Thread类中,有三个关于中断的核心方法,这就是Java中断的核心方法:
    在这里插入图片描述

    2、什么是中断机制

    一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运。
    所以,Thread.stop()、Thread.suspend、Thread.resume()等关于强制线程停止的方法都已经废弃了。

    在Java中,没有办法立即停止一个线程,然而停止线程本身这个功能是很重要的,比如说取消一个耗时的操作。
    因此,Java提供了一种用于停止线程的协商机制:中断,也即中断标识协商机制

    Java的中断只是一种协商机制,Java并没有给中断增加任何语法,中断的过程完全需要程序员自己来实现
    若要中断一个线程,我们需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设为true;接着我们需要自己写代码不断地在程序每一个关键点检测当前线程的标识位,如果为true,表示别的线程请求中断这条线程,此时究竟该做些什么,需要我们自己写代码实现。

    每个线程对象中都有一个中断标识位,用于标识线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt()方法将该线程的中断标识位设为true,可以在别的线程中调用,也可以在自己的线程中调用。

    3、如何理解中断机制

    当一个耗时操作被发起时,也许是请求方等不及了,或者又不需要继续执行下去了,我们此时想要中断这个线程,此时就可以使用Java的中断机制。

    比如说一个无烟商场,有个顾客正在一个角落抽烟,商场管理员能做的只能劝阻顾客:“请不要在此处抽烟,吸烟请到无烟区”。劝阻之后,是否继续吸烟完全由顾客来决定,顾客收到劝阻这个事件之后,同样可以做的不单单是“吸烟、不吸烟”这两个动作,也可以做其他任意的动作。

    但是,中断机制实现起来复杂,应用场景很窄,实际情况应用的非常少。

    4、三大中断方法源码详解

    (1)interrupt()

    实例方法:仅仅是将线程的中断标识设置为true,发起一个协商而不会立即停止线程。

    除非当前线程正在中断自身(这是始终允许checkAccess的),否则将调用此线程的方法,这可能会导致抛出 。SecurityException

    如果此线程在调用wait()类的 、 或方法或此类的 Object 、 join(long)join(long, int)sleep(long)wait(long)或wait(long, int)sleep(long, int)方法时join()被阻塞,则其中断状态将被清除,并且将收到 InterruptedException

    如果此线程在 的 InterruptibleChannel I/O 操作中被阻塞,则通道将关闭,线程的中断状态将被设置,并且线程将收到 java.nio.channels.ClosedByInterruptException。

    如果此线程在 中 java.nio.channels.Selector 被阻塞,则将设置线程的中断状态,并且它将立即从选择操作返回,可能具有非零值,就像调用选择器的方法 wakeup 一样。

    如果上述条件均不成立,则将设置此线程的中断状态。

    中断不活动的线程不需要有任何效果

    public void interrupt() {
        if (this != Thread.currentThread()) {
            checkAccess(); // 安全检查
    
            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupted = true; // 线程中断标识位设置为true
                    interrupt0();  // inform VM of interrupt
                    b.interrupt(this);
                    return;
                }
            }
        }
        interrupted = true;
        // inform VM of interrupt
        interrupt0();
    }
    
    private native void interrupt0();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (2)interrupted()

    静态方法:interrupted(),判断线程是否被中断,并清除当前中断状态。

    换句话说,如果要连续调用此方法两次,则第二次调用将返回 false(除非在第一个调用清除其中断状态之后,在第二个调用检查它之前,当前线程再次中断)。

    这个方法做了两件事:
    1.返回当前线程的中断状态,测试当前线程是否已被中断。
    2.将当前线程的中断状态清零并重新设为false,清除线程的中断状态。

    public static boolean interrupted() {
        Thread t = currentThread();
        boolean interrupted = t.interrupted;
        // We may have been interrupted the moment after we read the field,
        // so only clear the field if we saw that it was set and will return
        // true; otherwise we could lose an interrupt.
        if (interrupted) {
            t.interrupted = false;
            clearInterruptEvent();
        }
        return interrupted;
    }
    
    private static native void clearInterruptEvent();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (3)isInterrupted()

    实例方法:isInterrupted()

    判断当前线程是否被中断(通过检查中断标识位)

    public boolean isInterrupted() {
        return interrupted;
    }
    
    • 1
    • 2
    • 3

    (4)注意!不同jdk版本中源码的异同

    以上源码是jdk17版本,高版本jdk中,引入了一个内部变量interrupted作为线程中断标识,而jdk8完全是调用底层native方法来判断的,这无疑在性能上会有较大的差距。

    我们看一下jdk8的源码:

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();
    
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
    
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    
    
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
    
    private native boolean isInterrupted(boolean ClearInterrupted);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    二、动手实现线程的中断

    1、通过一个volatile变量实现

    public class InterruptDemo
    {
        static volatile boolean isStop = false;
    
        private static void main()
        {
            new Thread(() -> {
                while (true)
                {
                    if(isStop)
                    {
                        System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序停止");
                        break;
                    }
                    System.out.println("t1 -----hello volatile");
                }
            },"t1").start();
    
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    
            new Thread(() -> {
                isStop = true;
            },"t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    使用volatile可以保证变量的内存可见性,t1线程不断地在关键步骤处检测中断标识是否为true,当另一个线程将中断标识设为true时,t1线程检测到,并终止程序。

    2、通过AtomicBoolean实现

    public class InterruptDemo
    {
        static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    
        private static void main()
        {
            new Thread(() -> {
                while (true)
                {
                    if(atomicBoolean.get())
                    {
                        System.out.println(Thread.currentThread().getName()+"\t atomicBoolean被修改为true,程序停止");
                        break;
                    }
                    System.out.println("t1 -----hello atomicBoolean");
                }
            },"t1").start();
    
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    
            new Thread(() -> {
                atomicBoolean.set(true);
            },"t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    同样的,使用AtomicBoolean 原子类,可以实现通过一个变量充当线程中断标识。

    3、使用中断API

    public class InterruptDemo
    {
        public static void main(String[] args)
        {
            Thread t1 = new Thread(() -> {
                while (true)
                {
                    if(Thread.currentThread().isInterrupted())
                    {
                        System.out.println(Thread.currentThread().getName()+"\t isInterrupted()被修改为true,程序停止");
                        break;
                    }
                    System.out.println("t1 -----hello interrupt api");
                }
            }, "t1");
            t1.start();
    
            System.out.println("-----t1的默认中断标志位:"+t1.isInterrupted());
    
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
    
            //t2向t1发出协商,将t1的中断标志位设为true希望t1停下来
            new Thread(() -> {
                t1.interrupt();
            },"t2").start();
            //t1.interrupt();
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    此处我们使用线程中断的内置方法,实现了中断。

    4、证明:调用interrupt方法不会强制中断线程

    public class InterruptDemo2
    {
        public static void main(String[] args)
        {
            //实例方法interrupt()仅仅是设置线程的中断状态位设置为true,不会停止线程
            Thread t1 = new Thread(() -> {
                for (int i = 1; i <=300; i++)
                {
                    System.out.println("-----: "+i);
                }
                System.out.println("t1线程调用interrupt()后的的中断标识02:"+Thread.currentThread().isInterrupted());
            }, "t1");
            t1.start();
    
            System.out.println("t1线程默认的中断标识:"+t1.isInterrupted());//false
    
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            t1.interrupt();//true
            System.out.println("t1线程调用interrupt()后的的中断标识01:"+t1.isInterrupted());//true
    
            try { TimeUnit.MILLISECONDS.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("t1线程调用interrupt()后的的中断标识03:"+t1.isInterrupted());//????---false中断不活动的线程不会产生任何影响。
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    以上程序可以看出,我们调用线程的interrupt方法,将线程的中断标识位设为true,但是并不会立即停止该线程,因为该线程中并没有编写关于中断处理的逻辑。

    5、中断sleep、wait中的线程

    public class InterruptDemo3
    {
        public static void main(String[] args)
        {
            Thread t1 = new Thread(() -> {
                while (true)
                {
                    if(Thread.currentThread().isInterrupted())
                    {
                        System.out.println(Thread.currentThread().getName()+"\t " +
                                "中断标志位:"+Thread.currentThread().isInterrupted()+" 程序停止");
                        break;
                    }
    
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        //Thread.currentThread().interrupt();//为什么要在异常处,再调用一次??
                        e.printStackTrace();
                    }
    
                    System.out.println("-----hello InterruptDemo3");
                }
            }, "t1");
            t1.start();
    
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    
            new Thread(() -> t1.interrupt(),"t2").start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    我们发现,在sleep中的线程,被调用了interrupt方法之后,会抛出InterruptedException 中断休眠,此时中断状态会被清除,并将中断标识位设置为false。

    /**
    使当前正在执行的线程在指定的毫秒数内休眠(暂时停止执行),具体取决于系统计时器和计划程序的精度和准确性。线程不会失去任何监视器的所有权。
    参数:
    millis – 睡眠时间长度(以毫秒为单位)
    抛出:
    IllegalArgumentException – 如果 的值 millis 为负数
    InterruptedException – 如果任何线程中断了当前线程。引发此异常时,将清除当前线程的 中断状态 。
    */
    public static native void sleep(long millis) throws InterruptedException;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在catch块中,需要再次给中断标志位设置为true,需要手动再次将线程中断标识设为true,停止程序。

  • 相关阅读:
    数据仓库建模设计
    面试官:ThreadLocal使用场景有哪些?内存泄露问题如何避免?
    .Net Redis 入门到熟练
    Linux From Scratch 11.2 发布
    MSE和Video标签的关系
    操作系统,计算机网络,数据库刷题笔记2
    FFmepg使用指南
    DNA-CuInSeQDs近红外CuInSe量子点包裹脱氧核糖核酸DNA
    C# FileSystemWatcher 多文件夹、多文件类型文件监控增加、修改、重命名和删除实例
    知识图谱:知识的表示方法
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/133610381