java多线程之中断(interrupt)问题

摘要

在java中,想要让一个线程停下来,有三种办法:
(1)采用退出标志,使得run方法执行完之后线程自然终止。
(2)使用stop强行终止线程,但该方法由于安全问题已经被deprecated。
(3)使用中断机制。

引入

第一种方法没特别之处,无非是在覆盖Runnable接口之时对run方法中添加状态标识逻辑。比如:

public class MyThread extends Thread
{
    private boolean running;
    @Override
    public run(){
         if(running){
             //....business methods
         }
    }
}

下面详细讨论中断机制。
这里的中断是软中断,即在某个线程中可以将目标线程的中断标志位置位,然后在目标线程进行中断检查的时候来判断自己应当如何响应中断。
特别的,可以以此来停止目标线程。中断的局限性,无法对出于死锁状态的线程起作用。

简单原理:

java.lang.Thread类中提供了一个方法来将线程的中断标志位置位:
Thread.interrupt()方法,其函数申明为:public void interrupt();该方法会将线程的中断标志位置位。
java.lang.Thread类还两个方法来检查中断标志:

public static boolean interrupted()
{
    return currentThread().isInterrupted(true);
}
public boolean isInterrupted()
{
    return isInterrupted(false);
}

这两个方法分别调用同一个原生方法来检查中断状态并有所不同。
一方面interrupted()是静态方法,可以使用Thread.interrupted()的方法来调用。
isInterrupted() 则需要一个实例对象作为隐式参数,使用threadInstance.isInterrupted()来调用。
另一方面,interrupted在调用后会立即将线程中断标志位复位,而isInterrupted则不会这么做。

非阻塞线程的中断问题:

调用Thread.interrupt将目标线程中断标志位置位,然后在目标线程中检查中断标志位。
如果检查到中断之后使用throw new InterruptedException()抛出一个异常,然后使用catch语句来捕获这个异常,则可以在这里完成一些中断逻辑。
特别的,可以使用这种方法退出线程。下面是这种退出方式的举例。

package com.kingbaron.jinterrupt;
/**
 * Created by kingbaron on 2016/3/17.
 */
public class UnblockingInterrupt extends Thread{
    @Override
    public void run(){
        super.run();
        try{
            for(int i=0;i<10000;i++)
            {
                if(Thread.interrupted())
                //if(this.isInterrupted())
                {
                    System.out.println("I'm interrupted and I'm going out");
                    throw new InterruptedException();
                }
                System.out.println("i="+(i+1));
            }
            System.out.println("I'm below the for clauses and I have no chance to run here");
        }catch(InterruptedException e){
            System.out.println("I'm in the catch clauses of the method UnblockingInterrupt.run()");
            e.printStackTrace();
        }
    }
}
package com.kingbaron.jinterrupt;
/**
 * Created by kingbaron on 2016/3/17.
 */
public class TestUnblockingInterrupt {
    public static void main(String[] args){
        try{
            UnblockingInterrupt thread=new UnblockingInterrupt();
            thread.start();
            Thread.sleep(2000);
            thread.interrupt();
        }catch(InterruptedException e)
        {
            System.out.println("main catch");
            e.printStackTrace();
        }
    }
}

输出结果(关键部分)为:

i=277919
i=277920
I'm interrupted and I'm going out
I'm in the catch clauses of the method UnblockingInterrupt.run
java.lang.InterruptedException
at com.kingbaron.jinterrupt.UnblockingInterrupt.run(UnblockingInterrupt.java:16)

可以看到,当线程thread的中断标志位被main线程中使用thread.interrupt置位后,在执行thread线程的for循环的if条件检查时,
Thread.interrupted()返回true随后在其中抛出一个InterruptedException异常,再使用catch子句捕获该异常,由于run方法中在catch子句之后再无语句,
故线程从run方法返回,thread线程就此终止。

阻塞线程的中断问题。

首先,一个线程出于阻塞中断状态(实际上还包括等待状态)的原因,常见的有线程调用了thread.sleep、
thread.join、thread.wait、1.5中的condition.await、以及可中断的通道上的 I/O 操作方法等。
如果一个线程处于阻塞状态,若将其中断标志位置为,则会在产生阻塞的语句出抛出一个InterruptedException异常,
并且在抛出异常后立即将线程的中断标示位复位,即重新设置为false。
抛出异常是为了线程从阻塞状态醒过来,并在结束线程前让程序员有足够的时间来处理中断请求。
换言之,中断将一个处于阻塞状态的线程的阻塞状态解除。
注意,synchronized在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断(请参考后面的测试例子)。
与synchronized功能相似的reentrantLock.lock()方法也是一样,它也不可中断的。
即如果发生死锁,那么reentrantLock.lock()方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。
但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。
你也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock方法。
没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。
某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是更普遍的情况是,一个线程将把中断看作一个终止请求。
这种线程的run方法遵循如下形式:

public void run() {
    try {
        ...
        /*
         * 不管循环里是否调用过线程阻塞的方法如sleep、join、wait,这里还是需要加上
         * !Thread.currentThread().isInterrupted()条件,虽然抛出异常后退出了循环,显
         * 得用阻塞的情况下是多余的,但如果调用了阻塞方法但没有阻塞时,这样会更安全、更及时。
         */
        while (!Thread.currentThread().isInterrupted()&& more work to do) {
            do more work 
        }
    } catch (InterruptedException e) {
        //线程在wait或sleep期间被中断了
    } finally {
        //线程结束前做一些清理工作
    }
}

在这里,应当注意一个原则:尽量不要在底层代码中捕获InterruptedException原则。因为该异常一旦被捕获,线程的中断标志位就会被置位。
对于底层代码而言,如果代码会抛出不知道应当如何应对的InterruptException异常,可以有下面两个选择。
(1)捕获到InterruptedException异常后将中断标志位置位,让外层代码根据检查中断标志位来判断是否中断。

public subTask(){
    try{
        //...the works may throw a InterruptedException
    }catch(InterruptedException e){
        Thread.currentThread.interrupt();
    }
}

(2)更推荐的方法是,底层代码不捕获InterruptedException,直接将其抛给外层代码去解决

public subTask() throws InterruptedException
{
    //...
}

中断失效的情况之临界区:

进入临界区的代码是不允许中断的。这一点很好理解,临界区是并行问题为了保护临界资源的互斥访问而特地加锁的,
一旦可以中断,那么锁的存在也就没有任何意义了。特别的,形成了死锁的线程会分别处于阻塞状态,但是它们都无法被中断。
常见的有进入synchronized块的代码以及Lock.lock()之后没能得到锁而处于阻塞的状态。通常可以使用Lock.lockInterruptibly()来代替Lock.lock(),
因为Lock.lockInterruptibly()可以接受中断,这个中断指的是,既然没能得到锁进入临界区,与其阻塞不如做些别的什么事。
一旦进入临界区,任何方法都不得中断。

中断失效的情况之不可中断I/O:

自java 1.4之后对大量数据的I/O常用通道(channels)机制,它是可以被中断的I/O,也就是说如果这种I/O处于阻塞状态,可以使用中断来解除阻塞。
但是存在着一些不可中断的操作,比如ServerSocket.accept(),inputSteam.read()等调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。
对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开。
对于处理大型I/O时,推荐使用Channels。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值