Java 线程阻塞、中断及优雅退出

线程阻塞

一个线程进入阻塞状态的原因可能如下(已排除Deprecated方法):

sleep()

sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;

当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象锁并没有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。

wait()

调用wait()/1.5中的condition.await()使线程挂起,直到线程获取notify()/notifyAll()消息,(或者在Java SE5中java.util.concurrent类库中等价的signal()/signalAll()消息),线程才会进入就绪状态;

wait()调用会释放当前对象锁(monitor),这样其他线程可以继续进入对象的同步方法。参见上一篇文章线程间协作——wait & notify & notifyAll

另外,调用join()也会导致线程阻塞,因为源码中join()就是通过wait()实现的;

等待I/O;
class Demo3 implements Runnable throws InterruptedException{
     private InputStream in;
     public void run(){
          in.read();
     }
}
无法持有锁进入同步代码

进入同步代码前无法获取锁,比如试图调用synchronized方法,或者显示锁对象的上锁行为ReentrantLock.lock(),而对应锁已被其他线程获取的情况下都将导致线程进入阻塞状态;

注意:yield()并不会导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

线程中断

线程中断可以在线程内部设置一个中断标识,同时让处于(可中断)阻塞的线程抛出InterruptedException中断异常,使线程跳出阻塞状态。相比其他语言,Java线程中断比较特殊,经常会引起开发人员的误解。因为中断听起来高深复杂,实质原理上非常简单。

中断原理

Java中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断。这好比是家里的父母叮嘱在外的子女要注意身体,但子女是否注意身体,怎么注意身体则完全取决于自己。

Java中断模型也是这么简单,每个线程对象里都有一个boolean类型的标识(不一定就要是Thread类的字段,实际上也的确不是,这几个方法最终都是通过native方法来完成的),代表着是否有中断请求(该请求可以来自所有线程,包括被中断的线程本身)。例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。

中断相关的方法
方法解释
public static boolean interrupted()测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。
public boolean isInterrupted()测试线程是否已经中断。线程的中断状态不受该方法的影响。
public void interrupt()中断线程,设置中断标识为为true。

其中,interrupt方法是唯一能将中断状态设置为true的方法。静态方法interrupted会将当前线程的中断状态清除,但这个方法的命名极不直观,很容易造成误解,需要特别注意。

此外,类库中的有些类的方法也可能会调用中断,如FutureTask中的cancel方法,如果传入的参数为true,它将会在正在运行异步任务的线程上调用interrupt方法,如果正在执行的异步任务中的代码没有对中断做出响应,那么cancel方法中的参数将不会起到什么效果;

ExecutorService exec = Executors.newCachedThreadPool();
Futrue<?> f = exec.submit(new TaskThread());
f.interrupt();

又如ThreadPoolExecutor中的shutdownNow方法会遍历线程池中的工作线程并调用线程的interrupt方法来中断线程,所以如果工作线程中正在执行的任务没有对中断做出响应,任务将一直执行直到正常结束。

ExecutorService exec = Executors.newCachedThreadPool();
for(int i=0;i<5;i++)
     exec.execute(new TaskThread())
exec.shutdownNow();
中断的处理

中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。某些线程非常重要,以至于它们应该不理会中断,而是在处理完抛出的异常之后继续执行,但是更普遍的情况是,一个线程将把中断看作一个终止请求,这种线程的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 {
        //线程结束前做一些清理工作
    }
}

上面是while循环在try块里,如果try在while循环里时,因该在catch块里重新设置一下中断标示,因为抛出InterruptedException异常后,中断标示位会自动清除,此时应该这样:

public void run() {
    while (!Thread.currentThread().isInterrupted()&& more work to do) {
        try {
            ...
            sleep(delay);
            //wait(delay);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();   //重新设置中断标示
        }
    }
}
可中断阻塞

对于处于sleep,join等操作的线程,如果被调用interrupt()后,会抛出InterruptedException,然后线程的中断标志位会由true重置为false,因为线程为了处理异常已经重新处于就绪状态。

不可中断的操作,包括进入synchronized段以及Lock.lock(),inputSteam.read()等,调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。

对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。

对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开。

但是,你可能正使用Java1.0之前就存在的传统的I/O,Thread.interrupt()将不起作用,因为线程将不会退出被阻塞状态。

很幸运,Java平台为这种情形提供了一项解决方案,即调用阻塞该线程的套接字的close()方法。在这种情形下,如果线程被I/O操作阻塞,当调用该套接字的close方法时,该线程在调用accept地方法将接收到一个SocketException(SocketException为IOException的子异常)异常,这与使用interrupt()方法引起一个InterruptedException异常被抛出非常相似。

java.nio类库提供了更加人性化的I/O中断,被阻塞的nio通道会自动地响应中断,不需要关闭底层资源;

线程优雅退出

一般情况下,线程退出可以使用while循环判断共享变量条件的方式,当线程内有阻塞操作时,可能导致线程无法运行到条件判断的地方而导致一直阻塞下去,这个时候就需要中断来帮助线程脱离阻塞。因此比较优雅的退出线程方式是结合共享变量和中断。

thread = new Thread(new Runnable() {
    @Override
    public void run() {
        /*
         * 在这里为一个循环,条件是判断线程的中断标志位是否中断
         */
        while (flag&&(!Thread.currentThread().isInterrupted())) {
            try {
                Log.i("tag","线程运行中"+Thread.currentThread().getId());
                // 每执行一次暂停40毫秒
                //当sleep方法抛出InterruptedException  中断状态也会被清掉
                Thread.sleep(40);
            } catch (InterruptedException e) {
                e.printStackTrace();
                //如果抛出异常则再次设置中断请求
                Thread.currentThread().interrupt();
            }
        }
    }
});
thread.start();

参考

  1. 详细分析Java中断机制
  2. Thread的中断机制(interrupt)
  3. Java多线程之阻塞I/O如何中断
  4. 《Java 编程思想》
  • 3
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值