Java多线程---线程的取消和关闭

启动一个线程是很容易的,无论是继承Thread类或者实现Runnable方法。大多数时候我们都是希望它能正常运行直到结束,或者自行停止。然而有时候我们希望能够提前结束线程,这个可能是用户取消了某个操作,或者应用程序需要快速关闭。

Java语言本身没有提供任何机制来安全的终结某个线程。但是提供了中断(Interruption),这是一种协作机制,能使一个线程终止另一个线程。这种协作式的机制式式很有必要的,因为我们很少需要一个线程立即停止,这样会造成数据不一致的状态(这就是Thread.stop()为什么被弃用的原因)。当需要停止时,首先清除当前正在执行的工作,然后再结束。

取消一个操作的原因

  • 用户请求取消。比如点击 “取消” 按钮
  • 有时间限制的操作。比如在有限时间内搜索问题解空间,并得到最优解。当计时器超时的时候,取消正在搜索的任务。
  • 应用程序事件。将上述搜索问题解空间的任务分解,当一个任务找到最佳解决方案后,则其他正在执行搜索任务的线程将被取消。
  • 错误。比如一个爬虫在搜索网页,并保存到磁盘。当磁盘满时,或者其他错误,所有的搜索任务都会结束。
  • 关闭。当一个程序或服务关闭时,对正在执行的任务平缓的关闭(即等待其继续执行完成),或立即关闭(取消任务)。

取消标志

设置一个表示“已请求取消”的标志,然后定期地查看该标志。如果设置了这个标志那么任务将提前结束。下面举个例子:

public class SimpleCancle implements Runnable{
    private volatile boolean flag;
    public void cancle(){flag = true;}
    @Override
    public void run() {
        int i = 0;
        while (!flag) {
            System.out.println(i++);
        }
        System.out.println("run is finished");
    }
    public static void main(String[] args) {
        SimpleCancle sample = new SimpleCancle();
        Thread test = new Thread(sample);
        test.start();
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
       		sample.cancle();
       	}
    }
}

这里的while循环如果没有设置标志位,那么会一直执行,消耗cpu时钟周期。通过这种设置标志位的行为进行取消可能会存在一定的延迟,就是说在调用cancle方法和run方法执行下一次检查之间可能存在延迟。在finally块中保证一定能取消。

中断

上述使用取消表示确实可以停止任务并退出,虽然可能存在一定的延迟。但是如果while循环中调用了一个阻塞的方法,比如BlockingQueue.put,那么这个任务可能永远不会检查标志,因此永远不会结束。
举个例子:

public class NeverStop implements Runnable{
    private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);
    private volatile boolean flag;
    public void run() {
        while (!flag) {
            try {
                queue.put(new Random().nextInt());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void cancle() {flag = true;}
}

那么怎么解决这个问题呢?使用java的中断机制

中断机制

每一个线程都有一个中断状态。当中断一个线程时,这个状态变为true。Thread中包含了中断线程以及查询中断状态的方法,相关的代码如下:

public class Thread {
	public void interrupt() {......}
	public boolean isInterrupted() {......}
	public static boolean interrupted() {......}
}

其中interrupt能中断目标线程,isInterrupted能返回目标线程的中断状态。静态的interrupted将会清除当前线程的中断状态,并返回它之前的指,这也是清除中断状态的唯一方法。

阻塞库方法,例如Thread.sleep,Object.wait,BlockingQueue.put等,都会检查线程何时中断,并且在发现中断时提前返回。在响应中断时的操作包括:清除中断状态抛出InterruptedException,表示阻塞操作由于中断而提前结束。

因为Thread中的阻塞方法是直接通过native方法实现的。我们这里以阻塞队列的put为例,看看API是怎么封装的,put的阻塞实际是ReentrantLock中的lockInterruptibly()来实现,而lock框架依托于AQS,我们来展示一下:

BlockingQueue:
public void put(E e) throws InterruptedException {
		......
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly(); //可响应中断
        ......
}
ReentrantLock:
public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//调用了AQS的方法
}
AbstractQueuedSynchronizer:(AQS)
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())//清除中断状态
            throw new InterruptedException(); //抛异常
        ......
}

上面的代码只是方法代码中的一部分,只是想给大家展示一下响应中断的一个处理逻辑。还有一点就是JVM并不能保证阻塞方法检测到中断的速度,但是实际响应速度还是很快。

中断机制的正确理解

上面讲了一个非常棒的取消机制即中断,当需要中断时调用interrupt设置中断状态,如果时非阻塞时,它的中断状态将会被设置,然后线程根据被取消的操作来检查中断状态判断是否发生了中断。那么在这个过程中如果不触发InterruptException,那么中断状态将会一致保持,直到明确地清除中断状态

public void run() {
        try {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("此处中断");
                    throw new InterruptedException();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("out of while");
}

上面这段代码便没有明确的清除中断状态

需要注意的是,调用interrupt并不意味着立即停止目标线程,只是传递了请求中断的消息,然后由线程在下一个合适的时刻中断自己

像wait,sleep,join等方法将会严格的处理这个请求,即清除中断状态---->抛出异常。设计良好的代码可以忽略这种请求,只要能使得调用代码对中断请求进行处理即可,比如将这个请求传递给另一个任务;而设计糟糕得代码可能会屏蔽中断请求,从而导致其他的代码也无法对中断请求做出响应。

我们举一个利用中断来取消任务的栗子:

public class CanStop implements Runnable{
    private final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);

    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted())
                queue.put(new Random().nextInt());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("out of while");
    }

    public static void main(String[] args) throws InterruptedException {
        NeverStop sample = new NeverStop();
        Thread test = new Thread(sample);
        test.start();
        Thread.sleep(1000);
        System.out.println(test.isInterrupted());//false
        test.interrupt();
        System.out.println(test.isInterrupted());//true
        System.out.println(test.isInterrupted());//false
    }
}

通常,中断时实现取消的最合理方式

如何响应中断

由两种方式来处理InterruptException:

  • 传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的操作也成为可中断的阻塞方法。
  • 恢复中断异常,从而使调用栈的上层代码能够对其进行处理。

传递异常

private final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);
public Integer getInteger() throws InterruptedException {
        return queue.take();
    }

如果你不想或者无法传递InterruptedException(比如Runnable形式的任务),那么就需要另一种方式来保存中断请求。一种标准的方法就是通过再次调用interrupt恢复中断状态。不能屏蔽InterruptedException,除非在你代码中实现了线程的中断策略。

恢复中断异常
对于一些不支持取消但是仍可以调用可中断阻塞方法的操作,需要在循环中调用这些方法,并在发现这些中断后重新尝试。在这种情况下,他们应该在本地保存中断状态,并在返回前恢复状态,而不是在捕获异常时恢复状态。如下列程序所示,阻塞方法put会在入口除检查中断状态,如果已被设置则立即抛出异常,这样可能引起无限循环。代码如下:

public class CanStop{

    private final ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(20);

    public Integer getInteger() {
        boolean interrupt = false;
        try{
            while(true) {
                try{
                    return queue.take();
                }catch (InterruptedException e) {
                    interrupt = true;
                    System.out.println("exception occur");
                }
            }
        }finally {
            if(interrupt)
                Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException{
        CanStop can = new CanStop();
        Thread tt = new Thread(() -> {
            System.out.println(can.getInteger()+"  "+Thread.currentThread().isInterrupted());
        });
        tt.start();
        tt.interrupt();
        Thread.sleep(3000);
        can.queue.put(23);//加入一个值表示可以继续进行了
    }
}

总结

本篇讲了如何去取消一个任务线程
1、取消标志(但如果遇到阻塞情况,无能为力)
2、Java的中断机制,以及响应中断的两种方式。

猜你喜欢

转载自blog.csdn.net/machine_Heaven/article/details/104899697