Java多线程的中断机制

一.中断概述

这篇文章主要记录使用 interrupt() 方法中断线程,以及如何对InterruptedException进行处理。感觉对InterruptedException异常进行处理是一件谨慎且有技巧的活儿。

Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了

因为它们太暴力了,是不安全的,这种暴力中断线程是一种不安全的操作,因为线程占用的锁被强制释放,极易导致数据的不一致性。

举个栗子来说明其可能造成的问题。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        StopThread thread = new StopThread();
        thread.start();
        // 休眠1秒,确保线程进入运行
        Thread.sleep(1000);
        // 暂停线程
        thread.stop();
//      thread.interrupt();
        // 确保线程已经销毁
        while (thread.isAlive()) { }
        // 输出结果
        thread.print();
    }

    private static class StopThread extends Thread {

        private int x = 0; private int y = 0;

        @Override
        public void run() {
            // 这是一个同步原子操作
            synchronized (this) {
                ++x;
                try {
                    // 休眠3秒,模拟耗时操作
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ++y;
            }
        }

        public void print() {
            System.out.println("x=" + x + " y=" + y);
        }
    }
}
View Code

上述代码中,run方法里是一个同步的原子操作,x和y必须要共同增加,然而这里如果调用thread.stop()方法强制中断线程,输出如下:

x=1 y=0
没有异常,也破坏了我们的预期。
如果这种问题出现在我们的程序中,会引发难以预期的异常。因此这种不安全的方式很早就被废弃了。
取而代之的是interrupt(),上述代码如果采用thread.interrupt()方法,输出结果如下:
x=1 y=1
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at ThreadTest$StopThread.run(ThreadTest.java:28)

x=1,y=1 这个结果是符合我们的预期,同时还抛出了个异常。

interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」思想,是一个比较温柔的做法,它更类似一个标志位。

其实作用不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。

interrupt() 并不能真正的中断线程,这点要谨记。

需要被调用的线程自己进行配合才行。

例如:

 1 public class Run {
 2 
 3     public static void main(String[] args) {
 4         try {
 5             MyThread thread = new MyThread();
 6             thread.start();
 7             Thread.sleep(20);//modify 2000 to 20
 8             thread.interrupt();//请求中断MyThread线程
 9         } catch (InterruptedException e) {
10             System.out.println("main catch");
11             e.printStackTrace();
12         }
13         System.out.println("end!");
14     }
15 }

main线程睡眠20ms后,执行第8行中断MyThread线程

 1 public class MyThread extends Thread {
 2     @Override
 3     public void run() {
 4         super.run();
 5         for (int i = 0; i < 500000; i++) {
 6             if (this.interrupted()) {
 7                 System.out.println("should be stopped and exit");
 8                 break;
 9             }
10             System.out.println("i=" + (i + 1));
11         }
12         System.out.println("this line is also executed. thread does not stopped");//尽管线程被中断,但并没有结束运行。这行代码还是会被执
13     }
14 }

当MyThread获得CPU执行时,第6行的 if 测试中,检测到中断标识被设置。即MyThread线程检测到了main线程想要中断它的 请求。

大多数情况下,MyThread检测到了中断请求,对该中断的响应是:退出执行(或者说是结束执行)。

但是,上面第5至8行for循环,是执行break语句跳出for循环。但是,线程并没有结束,它只是跳出了for循环而已,它还会继续执行第12行的代码....

因此,我们的问题是,当收到了中断请求后,如何结束该线程呢?

种可行的方法是使用 return 语句 而不是 break语句。。。。。哈哈。。。

当然,一种更优雅的方式则是:抛出InterruptedException异常。

看下面MyThread类的代码:

 1 public class MyThread extends Thread {
 2     @Override
 3     public void run() {
 4         super.run();
 5         try{
 6             for (int i = 0; i < 500000; i++) {
 7                 if (this.interrupted()) {
 8                     System.out.println("should be stopped and exit");
 9                     throw new InterruptedException();
10                 }
11                 System.out.println("i=" + (i + 1));
12             }
13             System.out.println("this line cannot be executed. cause thread throws exception");//这行语句不会被执行!!!
14         }catch(InterruptedException e){
15             System.out.println("catch interrupted exception");
16             e.printStackTrace();
17         }
18     }
19 }

当MyThread线程检测到中断标识为true后,在第9行抛出InterruptedException异常。

这样,该线程就不能再执行其他的正常语句了(如,第13行语句不会执行)。

这里表明:interrupt()方法有两个作用,一个是将线程的中断状态置位(中断状态由false变成true);

另一个则是:让被中断的线程抛出InterruptedException异常。

这是很重要的。

这样,对于那些阻塞方法(比如 wait() 和 sleep())而言,当另一个线程调用interrupt()中断该线程时,该线程会从阻塞状态退出并且抛出中断异常

这样,我们就可以捕捉到中断异常,并根据实际情况对该线程从阻塞方法中异常退出而进行一些处理。

比如说:线程A获得了锁进入了同步代码块中,但由于条件不足调用 wait() 方法阻塞了。

这个时候,线程B执行 threadA.interrupt()请求中断线程A,此时线程A就会抛出InterruptedException,我们就可以在catch中捕获到这个异常并进行相应处理(比如进一步往上抛出)

因此,上面就是一个采用抛出异常的方式来结束线程的示例尽管该示例的实用性不大。原因:我们 生吞了中断

上面我们是在run()方法中抛出异常,符合这里描述的:

有时候抛出 InterruptedException 并不合适,例如当由 Runnable 定义的任务调用一个
可中断的方法时,就是如此。在这种情况下,不能重新抛出 InterruptedException,但是
您也不想什么都不做。当一个阻塞方法检测到中断并抛出 InterruptedException 时,它
清除中断状态。如果捕捉到 InterruptedException 但是不能重新抛出它,那么应该保留
中断发生的证据,以便调用栈中更高层的代码能知道中断,并对中断作出响应该任务可以
通过调用 interrupt() 以 “重新中断” 当前线程来完成,如清单 3 所示。 -----“摘自参考博文”

因为,run方法是实现的Runnable接口中的方法。不能像下面这样定义,也即上面所说的:“不能重新抛出InterruptedException”。

        @Override
        public void run() throws InterruptedException{//这是错误的
          //do something...

因此,一个更好的解决方案是:调用 interrupt() 以 “重新中断” 当前线程。改进MyThread类中catch异常的方式,如下:

 1 public class MyThread extends Thread {
 2     @Override
 3     public void run() {
 4         super.run();
 5         try{
 6             for (int i = 0; i < 500000; i++) {
 7                 if (this.interrupted()) {
 8                     System.out.println("should be stopped and exit");
 9                     throw new InterruptedException();
10                 }
11                 System.out.println("i=" + (i + 1));
12             }
13             System.out.println("this line cannot be executed. cause thread throws exception");
14         }catch(InterruptedException e){
15             /**这样处理不好
16              * System.out.println("catch interrupted exception");
17              * e.printStackTrace();
18              */
19              Thread.currentThread().interrupt();//这样处理比较好
20         }
21     }
22 }

这样,就由 生吞异常 变成了 将 异常事件 进一步扩散了。

二.中断相关三个的方法

  1. Thread.interrupt(): 设置中断状态为true           
  2. Thread.isInterrupted():获取中断状态
  3. Thread.interrupted():获取中断状态,并且清除中断状态(当然获取的是清除之前的值),也就是说连续两次调用此方法,第二次一定会返回false。

 对正在运行的线程调用interrupt(),并不会使线程停止运行,而只是让线程暂停一会。因为Thread.interrupt() 对正在运行的线程是不起作用的,只有对阻塞的线程有效

离开线程有三种常用的方法

1.在阻塞操作时如Thread.sleep()时被中断会抛出InterruptedException

Thread.interrupt()方法实际上只是设置了一个中断状态,当该线程由于下列原因而受阻时,这个中断状态就起作用了:

  (1)如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除它还将收到一个InterruptedException异常

这个时候,我们可以通过捕获 InterruptedException异常来终止线程的执行,具体可以通过return等退出或改变共享变量的值使其退出。
例2:线程在sleep时调用interrupt

/**
 * 通过线程sleep时调用Interrupt引发异常,停止线程的运行.
 * */
public class InterruptThread1 extends Thread {
    private double d = 0.0;

    public void run() {
    try {
        // 死循环执行打印"I am running!" 和做消耗时间的浮点计算
        while (true) {
            System.out.println("I am running!");

            for (int i = 0; i < 9; i++) {
                d = d + (Math.PI + Math.E) / d;
            }
            //休眠一断时间,中断时会抛出InterruptedException
            Thread.sleep(50);
        }
     }catch (InterruptedException e) {
         System.out.println("InterruptThread1.run() interrupted!");
    }
    }

    public static void main(String[] args) throws Exception {
        // 将任务交给一个线程执行
        InterruptThread1 t = new InterruptThread1();
        t.start();

        // 运行一断时间中断线程
        Thread.sleep(100);
        System.out.println("****************************");
        System.out.println("Interrupted Thread!");
        System.out.println("****************************");
        t.interrupt();
    }
}

输出结果:

I am running!
I am running!
I am running!
****************************
Interrupted Thread!
****************************
InterruptThread1.run() interrupted!

设置为interrupt中断标记后,运行到sleep方法时,会抛出异常。

      (2)如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException,而不是InterruptedException 异常
     
      (3)如果使用Java1.0之前就存在的传统的I/O操作,并且线程处于阻塞状态,Thread.interrupt()将不起作用,线程并不能退出阻塞状态。
               例如对于socket,通过调用阻塞该线程的套接字的close()方法。如果线程被I/O操作阻塞,该线程将接收到一个SocketException异常,这与使用interrupt()方法引起一个InterruptedException异常被抛出非常相似

通过SocketException异常中断阻塞线程

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 通过SocketException异常中断阻塞线程.
 * */
public class InterruptThread2 extends Thread {
    volatile boolean stop = false;
    volatile ServerSocket socket;

    public void run() {
        try {
            socket = new ServerSocket(7856);
        } catch (IOException e) {
            System.out.println("Could not create the socket...");
            return;
        }
        while (!stop) {
            System.out.println("Waiting for connection...");
            try {
                Socket sock = socket.accept();
            } catch (IOException e) {
                System.out.println("accept() failed or interrupted...");
            }
        }
        System.out.println("Thread exiting under request...");
    }

    public static void main(String args[]) throws Exception {
        InterruptThread2 thread = new InterruptThread2();
        System.out.println("Starting thread...");
        thread.start();
        Thread.sleep(3000);
        System.out.println("Asking thread to stop...");
        
        /*由于线程处理阻塞状态,interrupt不产生任何作用*/
        //System.out.println( "Interrupting thread..." );
        //thread.interrupt();
        
        thread.stop = true;
        thread.socket.close();
        Thread.sleep(3000);
        System.out.println("Stopping application...");

    }
}

运行结果

    1. Starting thread...
    2. Waiting for connection...
    3. Asking thread to stop...
    4. accept() failed or interrupted...
    5. Thread exiting under request...
    6. Stopping application...

2.Thread.interrupted()检查是否发生中断

对于正在运行的线程,可以调用thread.interrupt()通过Thread.interrupted()能告诉你线程是否发生中断,并将清除中断状态标记,所以程序不会两次通知你线程发生了中断
      详见《例3:通过interrupted中断线程

例3:通过interrupted中断线程

/**
 * 通过interrupted中断线程,停止线程的执行.
 *
 */
public class InterruptThread1 extends Thread {
    private double d = 0.0;

    public void run() {
        // 检查程序是否发生中断
        while (!Thread.interrupted()) {
            System.out.println("I am running!");

            for (int i = 0; i < 90; i++) {
                d = d + (Math.PI + Math.E) / d;
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // 将任务交给一个线程执行
        InterruptThread1 t = new InterruptThread1();
        t.start();

        // 运行一断时间中断线程
        Thread.sleep(100);
        System.out.println("****************************");
        System.out.println("Interrupted Thread!");
        System.out.println("****************************");
        t.interrupt();
    }
}

运行结果

  1. I am running!
  2. I am running!
  3. ****************************
  4. Interrupted Thread!
  5. ****************************

 如果sleep和interrupted检查结合使用,可能会产生两个结果。
      详见《例4: 通过interrupted和sleep中断线程,停止线程的执行

例4: 通过interrupted和sleep中断线程,停止线程的执行

/**
 * 通过interrupted和sleep中断线程,停止线程的执行.
 *
 */
public class InterruptThread1 extends Thread {
    private double d = 0.0;

    public void run() {
        try {
            // 检查程序是否发生中断
            while (!Thread.interrupted()) {
                System.out.println("I am running!");
                // before sleep
                Thread.sleep(20);
                //after sleep
                System.out.println("Calculating");
                for (int i = 0; i < 900; i++) {
                    d = d + (Math.PI + Math.E) / d;
                }
            }

        } catch (InterruptedException e) {
            System.out.println("InterruptThread1.run() Exception!");
        }

        System.out.println("InterruptThread1.run() end!");
    }

    public static void main(String[] args) throws Exception {
        // 将任务交给一个线程执行
        InterruptThread1 t = new InterruptThread1();
        t.start();

        // 运行一段时间中断线程
        Thread.sleep(200);
        System.out.println("****************************");
        System.out.println("Interrupted Thread!");
        System.out.println("****************************");
        t.interrupt();
    }
}

如果在睡眠之前产生中断,则调用Thread.sleep()时抛出InterruptedException,结束线程

如果在睡眠之后产生中断,则线程会继续执行到下一次while判断中断状态时,结束线程

3.使用共享变量控制

使用共享变量(shared variable)发出信号,告诉线程必须停止正在运行的任务。线程必须周期性的核查这一变量(尤其在冗余操作期间),然后有秩序地中止任务。
      详见《例5:通过共享变量中断线程,停止线程的执行
例5:通过共享变量中断线程,停止线程的执行

/**
 * 通过共享变量中断线程,停止线程的执行.
 * @version V1.0 ,2011-4-15
 * @author xiahui
 */
public class InterruptThread1 extends Thread {
    private double d = 0.0;
    volatile boolean stop = false;
    
    public void run() {
        // 检查程序是否发生中断
        while (!stop) {
            System.out.println("I am running!");

            for (int i = 0; i < 900; i++) {
                d = d + (Math.PI + Math.E) / d;
            }
        }
        //做一些清理工作
        System.out.println( "Thread is exiting under request..." );
    }

    public static void main(String[] args) throws Exception {
        // 将任务交给一个线程执行
        InterruptThread1 t = new InterruptThread1();
        t.start();

        // 运行一段时间中断线程
        Thread.sleep(100);
        System.out.println( "Asking thread to stop..." );
        t.stop = true;

        Thread.sleep(1000 );
        System.out.println( "Stopping application..." );
    }
}

这个方法可以给予线程机会进行必要的清理工作,这在任何一个多线程应用程序中都是绝对需要的。

请确认将共享变量定义成volatile 类型或将对它的一切访问封入同步的块/方法(synchronized blocks/methods)中。

      但是,当线程等待某些事件发生而被阻塞,又会发生什么?当然,如果线程被阻塞,它便不能核查共享变量,也就不能停止。
      他们都可能永久的阻塞线程。即使发生超时,在超时期满之前持续等待也是不可行和不适当的,所以,要使用某种机制使得线程更早地退出被阻塞的状态

 参考:

https://www.cnblogs.com/hapjin/p/5450779.html

http://blog.chinaunix.net/uid-122937-id-215995.html

https://www.jb51.net/article/128207.htm

https://www.jianshu.com/p/e0ff2e420ab6

原文地址:https://www.cnblogs.com/Vincent-yuan/p/15106607.html