Java学习笔记——线程的中断(interrupt)

本文详细介绍了Java线程中断的原理和实践,包括如何发起中断请求、线程如何检测并处理中断信号,以及中断信号对程序的影响。通过实例解析了线程中断在多线程环境中的应用,强调了volatile关键字在并发中的作用,并讨论了线程间共享变量的可见性问题。
摘要由CSDN通过智能技术生成

Java学习笔记——线程的中断(interrupt)

本文是本人学习廖雪峰Java教程Oracle Java教程的线程中断部分后写的学习笔记,总结了一些线程中断方面的技术要点,分析了廖雪峰教程中提供的案例。

1.如何对一个线程发起中断请求

有两种常见的对某个线程发起中断信号的方法:

  1. 调用线程对象的interrupt方法。例如:threadObj.interrupt()。
  2. 事先在线程类中自定义一个flag变量,表示线程的运行状态。若想通知线程自我中断,则改变flag变量的值。

对于第二种方法,flag变量需为volatile变量,以便调用线程和被调用线程变量值统一。

2.谁来决定线程是否中断

需要注意的是,interrupt方法和flag变量只能发送中断信号,不能强制中断一个线程。线程检查到中断信号后,自行决定是否中断(大部分情况下,线程**“应该”**在检测到中断信号后中断)。

3.线程如何检测收到的中断信号

主要有四种检测方式:

  1. 调用 Thread.interrupted():此方法返回调用环境所在的线程的中断信号;
  2. 调用 this.isInterrupted(): 获取this的中断信号;
  3. 处理 InterruptedException 类异常:Thread.sleep(), subThreadObj.join()等让调用环境线程进入等待状态的方法会在调用环境线程被interrupt()时抛出InterruptedException。
  4. 检查自定义flag位的值。

4.一个线程该如何处理中断

所谓处理中断,就是提供一个中断情况下的返回路径。应结合具体情况具体分析。例如,检查文件流是否被妥善关闭、结束子线程等。

5.检查对中断信号的值的影响

不同的检查方式对中断信号值有不同影响:

  1. Thread.interrupted():检查后线程的中断状态被清除,也就是说,若下次检查前线程的中断状态不再被设为已中断,则再次调用Thread.interrupted()返回的是true。
  2. isInterrupted(): 不清除中断状态
  3. 检查自定义flag值:显然,单纯的访问flag成员不会改变它的值。
  4. InterruptedException: 异常被捕获后,线程的中断状态被清除。

5.案例分析

5.1

public class TestMultiThread1 {
    public static void main(String[] args) throws InterruptedException{
        Thread t = new MyThread1();
        t.start();
        Thread.sleep(500);
        t.interrupt(); // 中断t线程
        t.join(); //join使main线程停止,t线程执行结束时,join才会返回,主线程得以继续。
        System.out.println("end");
    }
}

class MyThread1 extends Thread {
    public void run() {

        Thread hello = new HelloThread();
        hello.start(); 
        try {
            hello.join(); // 等待hello线程结束
        } catch (InterruptedException e) {
            System.out.println("MyThread interrupted");
        }
        hello.interrupt();
    }
}

class HelloThread extends Thread {
    public void run() {
        int n = 0;
        while (!isInterrupted()) {
            n++;
            System.out.println(n + " hello!");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("HelloThread interrupted");
                System.out.println(isInterrupted());
                break;
            }
        }
    }
}

输出如下:

1 hello!
2 hello!
3 hello!
4 hello!
5 hello!
MyThread interrupted
end
HelloThread interrupted
false

主线程sleep了500毫秒,hello线程打印hello的时间间隔为100毫秒,t和hello的构造等也有一些时间开销。在主线程sleep结束前,hello线程完成了四次循环,打印了5次hello。

主线程sleep结束后,调用t的interrupt()。这造成两个影响:

  1. 主线程进而执行t.join,主线程进入等待状态,当t.run执行完毕后主线程才能继续;
  2. t.run中的hello.join语句抛出中断异常,t的等待状态被解除;

hello.join抛出异常后,catch块打印了"MyThread interrupted"。异常处理流程结束,紧接着调用hello.interrupt()结束子进程hello。这造成两个影响:

  1. t.run执行结束,主线程中的t.join()返回, 继而主线程结束,打印 “end”;
  2. 此时,hello线程正在其run函数的第五个循环中等待sleep结束。由于被interrupt了,sleep函数立即抛出异常。捕获异常后,打印 “HelloThread interrupted”。InterruptedException被捕获后,中断状态被清除,所以isInterrupted()打印 “false”。最后,流程在break语句跳出循环,hello线程结束。

需要注意:HelloThread中while循环的循环条件(!isInterrupted())几乎没有任何用处。绝大部分时间,hello线程都在sleep中,因此,hello线程收到中断信号时的反应大概率是在sleep函数中抛出异常。而异常抛出后,中断状态被立即清除,如果不通过break语句跳出循环,进入下一次循环后,“ while(!isInterrupted())” 无法检测出中断状态,这样就形成了死循环。恰好在sleep结束后收到中断信号且被isInterrupted()检出的概率极小。

5.2

public class Main {
    public static void main(String[] args)  throws InterruptedException {
        HelloThread t = new HelloThread();
        t.start();
        Thread.sleep(1);
        t.running = false; // 标志位置为false
    }
}

class HelloThread extends Thread {
    public volatile boolean running = true;
    public void run() {
        int n = 0;
        while (running) {
            n ++;
            System.out.println(n + " hello!");
        }
        System.out.println("end!");
    }
}
廖雪峰教程是这样解释的:

注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。因此,volatile关键字的目的是告诉虚拟机:每次访问变量时,总是获取主内存的最新值; 每次修改变量后,立刻回写到主内存。volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值