一文搞懂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 一样。

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

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

java 复制代码
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();

(2)interrupted()

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

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

这个方法做了两件事:

1.返回当前线程的中断状态,测试当前线程是否已被中断。

2.将当前线程的中断状态清零并重新设为false,清除线程的中断状态。

java 复制代码
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();

(3)isInterrupted()

实例方法:isInterrupted()

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

java 复制代码
public boolean isInterrupted() {
    return interrupted;
}

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

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

我们看一下jdk8的源码:

java 复制代码
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、通过一个volatile变量实现

java 复制代码
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();
    }
}

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

2、通过AtomicBoolean实现

java 复制代码
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();
    }
}

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

3、使用中断API

java 复制代码
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();

    }
}

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

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

java 复制代码
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中断不活动的线程不会产生任何影响。
    }
}

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

5、中断sleep、wait中的线程

java 复制代码
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();
    }
}

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

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

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

相关推荐
程序员陆通26 分钟前
Spring Boot RESTful API开发教程
spring boot·后端·restful
无理 Java1 小时前
【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)
java·后端·spring·面试·mvc·框架·springmvc
cyz1410012 小时前
vue3+vite@4+ts+elementplus创建项目详解
开发语言·后端·rust
liuxin334455662 小时前
大学生就业招聘:Spring Boot系统的高效实现
spring boot·后端·mfc
向上的车轮3 小时前
ASP.NET Zero 多租户介绍
后端·asp.net·saas·多租户
yz_518 Nemo3 小时前
django的路由分发
后端·python·django
AIRust编程之星3 小时前
Rust中的远程过程调用实现与实践
后端
Stark、4 小时前
异常处理【C++提升】(基本思想,重要概念,异常处理的函数机制、异常机制,栈解旋......你想要的全都有)
c语言·开发语言·c++·后端·异常处理
逢生博客5 小时前
Rust 语言开发 ESP32C3 并在 Wokwi 电子模拟器上运行(esp-hal 非标准库、LCD1602、I2C)
开发语言·后端·嵌入式硬件·rust
椰椰椰耶5 小时前
【Spring】@RequestMapping、@RestController和Postman
java·后端·spring·mvc