【java并发编程】interrupt()陷阱和线程正确的终止方式

一、背景

 在学习Java多线程知识时,掌握线程的启动和终止是应用基础。但由于多线程的复杂性,导致简单的线程终止程序出现意外的行为以及细微的、难以发现的错误。

  首先还是大概的罗列下停止线程的方法:

1、使用stop()方法:由于安全问题,已经不再被推荐使用,和suspend、resume一样。

2、使用退出标志位终止线程:引入一个共享变量,volatile类型或者使用synchronized来监视共享变量相关操作的方法,然后在run()方法中,通过while循环不停的轮询这个标志。

3、使用Interrupt()方法中断线程:中断状态是线程的一个标识位,而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。

      那么,通常情况下程序员将使用2和3两种方式来终止线程,这也是《java并发编程艺术》一书中所推荐的方式。但是对于刚接触多线程的同学来说,还是很容易被Thread.interrupt所迷惑。尽管,其名称似乎在暗示着什么,然而,这种方法并不会中断一个正在运行的线程 。

例如下面代码所描述的情况,它创建了一个线程,并且试图使用Thread.interrupt方法停止该线程。Thread.sleep()方法的调用,为线程的初 始化和中止提供了充裕的时间。线程本身并不参与任何有用的操作。 

class Example extends Thread {  
      //退出标志位
      private boolean stop=false;  
      
      public static void main( String args[] ) throws Exception {  
            Example thread = new Example1();  
            System.out.println( "Starting thread..." );  
            thread.start();  
            Thread.sleep( 3000 );  

            System.out.println( "Interrupting thread..." );  
            thread.interrupt();  

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

      public void run() {  
            while(!stop){  
                System.out.println( "Thread is running..." );  
                long time = System.currentTimeMillis();  
                while((System.currentTimeMillis()-time < 1000)) {  
                }  
            }  
            System.out.println("Thread exiting under request..." );  
      }  
}  

控制台看到以下输出: 

Starting thread... 

Thread is running... 

Thread is running... 

Thread is running... 

Interrupting thread... 

Thread is running... 

Thread is running... 

Thread is running... 

Stopping application... 

Thread is running... 

Thread is running... 

Thread is running... 
............................... 

很显然thread.interrrupt()这种方法并没有中断正在运行的线程 。

接下来,我将通过对Thread.interrupt()的详解,来探讨怎么让终止线程的做法显得更加安全和优雅。


二、interrupt()详解

2.1 interrupt JDK文档

关于interrupt(),java的djk文档描述如下:http://docs.oracle.com/javase/7/docs/api/

Interrupts this thread.
Unless the current thread is interrupting itself, which is always permitted, the checkAccess method of this thread is invoked, which may cause a SecurityException to be thrown.

If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.

If this thread is blocked in an I/O operation upon an interruptible channel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a ClosedByInterruptException.

If this thread is blocked in a Selector then the thread's interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector's wakeup method were invoked.

If none of the previous conditions hold then this thread's interrupt status will be set.

Interrupting a thread that is not alive need not have any effect.

中文翻译

interrupt()的作用是中断本线程。
本线程中断自己是被允许的;其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。
如果本线程是处于阻塞状态:调用线程的wait(), wait(long)或wait(long, int)会让它进入等待(阻塞)状态,或者调用线程的join(), join(long), join(long, int), sleep(long), sleep(long, int)也会让它进入阻塞状态。若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。例如,线程通过wait()进入阻塞状态,此时通过interrupt()中断该线程;调用interrupt()会立即将线程的中断标记设为“true”,但是由于线程处于阻塞状态,所以该“中断标记”会立即被清除为“false”,同时,会产生一个InterruptedException的异常。
如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。
如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”。
中断一个“已终止的线程”不会产生任何操作。

白话文

调用一个线程的Interrupt方法会把线程的状态改为中断态。这其中又可以细分成两个方面:

1) 对于因执行了sleep、wait、join方法而阻塞的线程:调用Interrupt方法会使他们不再阻塞,同时会抛出 InterruptedException异常。并且中断标志被清除,重新设置为false。比如一个线程A正在sleep中,这时候另外一个程序里去调用A的interrupt方法,这时就会迫使A停止休眠而抛出InterruptedException异常,从而提前使线程逃离阻塞状态。

2.)对于正在运行的线程,即没有阻塞的线程,调用Interrupt方法就只是把线程A的状态改为interruptted,但是不会影响线程A的继续执行,除非线程A将interruptted状态位作为执行判断符。

也就是说Interrupt只能有效地终止阻塞状态下的线程,对于运行状态下的线程需要退出标志位来终止。下面结合代码来说明如何终止两种状态下的线程

2.2 终止处于“阻塞状态”的线程

通常,我们通过“中断”方式终止处于“阻塞状态”的线程。
当线程由于被调用了sleep(), wait(), join()等方法而进入阻塞状态;若此时调用线程的interrupt()将线程的中断标记设为true。由于处于阻塞状态,中断标记会被清除(false),同时产生一个InterruptedException异常。将InterruptedException放在适当的为止就能终止线程,形式如下:

public class myThread extends Thread{

    @Override
    public void run() {
        try {
            while (true) {
                System.out.println( "Thread is running..." );
                // 执行任务...
                Thread.sleep(1000);//lock.wait()
                .....
            }
        } catch (InterruptedException ie) {  
            // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
            System.out.println("Thread exiting under request... ")
        }
 }
public class ThreadExample  {  
      
      public static void main( String args[] ) throws Exception {  
            MyThread thread = new EMyThread;  
            System.out.println( "Starting thread..." );  
            thread.start();  
            Thread.sleep( 3000 );  

            System.out.println( "Interrupting thread..." );  
            thread.interrupt();  

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

控制台打印

Starting thread... 

Thread running... 

Thread running... 

Thread running... 
 

Thread interrupted... 

Thread exiting under request... 

Stopping application... 

说明:在while(true)中不断的执行任务,当线程处于阻塞状态时,调用线程的interrupt()产生InterruptedException中断。中断的捕获在while(true)之外,这样就退出了while(true)循环!
注意:对InterruptedException的捕获务一般放在while(true)循环体的外面,这样,在产生异常时就退出了while(true)循环。否则,InterruptedException在while(true)循环体之内,就需要额外的添加退出处理,比如break。

2.3 终止处于“运行状态”的线程

通常,我们通过“标记”方式终止处于“运行状态”的线程。

其中,包括“中断标记”和“额外添加标记”。

1. 通过“中断标记”终止线程。

形式如下:

public class myThread extends Thread{

    @Override
    public void run() {
        while (!isInterrupted()) {
        // 执行任务...
        }
    }
}
public class ThreadShutdown {
    public static void main(String[] args) throws Exception {
       
        Thread myThread = new MyThread();
        myThread.start();
        
        //睡眠3秒,main线程对myThread进行中断,使myThread能够感知中断而结束
        Thread.sleep(3000);
        myThread.interrupt();
      
}

说明:isInterrupted()是判断线程的中断标记是不是为true。当线程处于运行状态,并且我们需要终止它时;可以调用线程的interrupt()方法,使用线程的中断标记为true,即isInterrupted()会返回true。此时,就会退出while循环。
注意:interrupt()并不会终止处于“运行状态”的线程!它会将线程的中断标记设为true。

2. 通过“额外添加标记”。

形式如下:

对背景中的代码优化后:

class Example2 extends Thread {  
   
   volatile boolean stop = false;  

   public static void main( String args[] ) throws Exception {  
       Example2 thread = new Example2();  
       System.out.println( "Starting thread..." );  
       thread.start(); 
 
       Thread.sleep( 3000 );  
       System.out.println( "Asking thread to stop..." );  
  
       thread.stop = true;  
       Thread.sleep( 3000 );  
       System.out.println( "Stopping application..." );  
   
  }  
  
  public void run() {  
       while ( !stop ) {  
           System.out.println( "Thread is running..." );  
           long time = System.currentTimeMillis();  
           
           while ( (System.currentTimeMillis()-time < 1000) && (!stop) ) { 
               //执行任务...
           }  
       }  
       System.out.println( "Thread exiting under request..." );  
  }  

}  

控制台打印:

Starting thread... 

Thread is running... 

Thread is running... 

Thread is running... 

Asking thread to stop... 

Thread exiting under request... 

Stopping application... 

      虽然该方法要求一些编码,但并不难实现。同时,它给予线程机会进行必要的清理工作,这在任何一个多线程应用程序中都是绝对需要的。请确认将共享变量定义成 volatile 类型或将对它的一切访问封入同步的块/方法(synchronized blocks/methods)中。

我们可以总结,调用线程类的interrupted方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted方法真正实现线程的中断原理是:开发人员根据中断标志的具体值,来决定如何退出线程。


三、安全的终止线程

      通过上面的分析,正常运行中的线程可以通过退出标记安全的终止线程,当然,如果线程在运行过程中被阻塞,它便不能核查共享变量,也就不能停止。这在许多情况下会 发生,例如调用Object.wait()、ServerSocket.accept()和DatagramSocket.receive()时。 这时就需要interrupt()抛出的InterruptedException来中断线程。所以,综合线程处于“阻塞状态”和“运行状态”的终止方式,是比较通用的安全终止线程的形式。

@Override
public void run() {
    try {
        // 1. isInterrupted()保证,只要中断标记为true就终止线程。
        while (!isInterrupted()) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {  
        // 2. InterruptedException异常保证,当InterruptedException异常产生时,线程被终止。
    }
}

对背景代码再一次修改

class Example3 extends Thread {  
   volatile boolean stop = false;
  
   public static void main( String args[] ) throws Exception {  
       Example3 thread = new Example3();  
       System.out.println( "Starting thread..." );  
       thread.start();  

       Thread.sleep( 3000 );  
       System.out.println( "Asking thread to stop..." );  
       thread.stop = true;//如果线程阻塞,将不会检查此变量  
       thread.interrupt();  
       Thread.sleep( 3000 );  
       System.out.println( "Stopping application..." );  
   
   }  
  
   public void run() { 
       //只要两个中断标记之一为true就终止线程。 
       while ( !stop && !Thread.currentThread().isInterrupted()) {  
           System.out.println( "Thread running..." );  
           try {  
               // 执行任务...
               // 由于某些原因可能被阻塞  
           } catch ( InterruptedException e ) {  
               //InterruptedException异常保证,当异常产生时,线程被终止。
               System.out.println( "Thread interrupted..." );  
           }  
       }  
       System.out.println( "Thread exiting under request..." );  
   }  
}  



 

发布了32 篇原创文章 · 获赞 15 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_42022528/article/details/88774485