如何优雅的中断线程

废弃的做法-stop/suspend/resume

​ JDK1.0中定义了stop/suspend/resume方法,用于中止一个正在运行的线程;其中,stop用于彻底停止当前运行的线程,suspend是暂停当前运行线程,一直阻塞到其他线程调用resume方法。

​ 但是,JDK1.2开始,这几个方法都被废弃了,原因如下:

  1. stop方法会立即终止当前运行的线程,从而导致业务逻辑执行不完整。

  2. stop方法会立即释放独占锁资源,但是无法保证锁内代码块的原子性。

    案例如下所示:

    public class ThreadTest {
    
        private Object object = new Object();
    
        private Integer i = 0;
    
        @Test
        public void test1() throws InterruptedException {
    
            Thread t1 = new Thread(()->{
                synchronized (object) {
                    i++;
                    System.out.println("i = " + i);
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    i--;
                    System.out.println("i = " + i);
                }
            });
            t1.start();
            TimeUnit.SECONDS.sleep(2);
            t1.stop();
        }
    }
    

    上述代码的逻辑很简单,即主线程中开启新的线程t1,t1中休眠10s,主线程休眠2s后调用stop方法,输出的结果为:

    i = 1;

    这对于业务来说是无法忍受的,如果stop方法之后要执行的逻辑是释放资源等清理性的工作,那么这些工作将永远无法被执行;而且对于使用同步机制的业务来说,自然是想保证共享数据的一致性,但是由于stop方法显然破坏了这一点。

  3. suspend和resume需要成对出现,否则极容易出现死锁。线程在执行suspend方法后,并不会释放锁,此时如果另外一个线程需要先获取该锁资源再去resume目标线程,那么就会发生死锁。(如果想实现暂停-唤醒的逻辑,可以通过设置一个标志位(volatile),表示该线程是应该运行还是挂起,如果是挂起,那么就调用wait方法使其等待;如果该运行,即恢复,则调用notify重新启动线程。wait方法与suspend不同的是,wait方法会释放锁资源,再被notify后需要重新参与锁的竞争~)

中断线程的方法

自定义标志位

​ 通过自定义变量作为状态位,定期检查该变量,符合条件则继续执行,否则退出。该方法适应于正在运行的线程

​ 将一个大任务分割为多个小任务,每次小任务执行完成后都会去校验一个状态标志位是否为true,如果非true,那么线程终止,不再继续执行任务。(状态标志位为volatile,保证可见性)

public class ThreadTest2 {

    private static volatile boolean isRun = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            int batch = 0;
            while (isRun) {
                System.out.println("==========" + batch++);
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        //终止线程
        stop();
        TimeUnit.SECONDS.sleep(10000);
    }

    public static void stop() {
        isRun = false;
    }
}

Interrupt()方法

​ 该方法适应于非正在运行线程的退出,对于大部分阻塞线程的方法,通过Thread.interrupt()可以立刻退出等待,抛出InterruptedException,包含sleep、join、wait和Lock.lockInterruptibly()、NIO阻塞等;

​ 如果你用了线程池,并使用了Future对象,可以使用future.cancel(boolean)方法取消正在执行的任务,false会等待正在执行的任务执行完成,但是会取消还没开始执行的任务;true表示会中断正在执行且能够响应中断异常的任务!!

案例

​ case1:一直运行的线程无法被中断

    @Test
    public void test0() throws InterruptedException {
        Thread t1 = getThread0();
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();
        System.out.println("thread isInterrupted = " + t1.isInterrupted());
        TimeUnit.SECONDS.sleep(10);
    }
    public Thread getThread0() {
        return new Thread(() -> {
            int count = 0;
            while (true) {
                System.out.println("=======" + count++);
            }
        });
    }

​ 运行结果:一直运行,直至10s后test1方法结束;

​ case2:线程被中断

    @Test
    public void test1() throws InterruptedException {
        Thread t2 = getThread1();
        t2.start();
        TimeUnit.SECONDS.sleep(2);
        t2.interrupt();
        System.out.println("thread isInterrupted = " + t2.isInterrupted());
        TimeUnit.SECONDS.sleep(10);
    }
    public Thread getThread1() {
        return new Thread(() -> {
            int count = 0;
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("==========" + count++);
            }
            System.out.println(Thread.currentThread().getName() + " is interrupted");
        });
    }

​ 运行结果:调用完interrupt方法后,线程中断结束;

Interrupt实现机制

​ 探寻其实现机制,首先要从源码入手~

public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

​ 首先判断执行中断的线程是不是自身被中断的线程,如果是由其他线程进行中断,会调用checkAccess()方法进行校验其他线程是否有权限对当前线程进行修改,源码如下:

    public final void checkAccess() {
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkAccess(this);
        }
    }

​ SecurityManager是java提供的安全管理器,目的是在运行阶段检查应用访问资源的权限,保护系统免受恶意操作攻击。可以通过jvm配置或者编码方式启动安全管理器,若没有指定policy文件,那么会加载默认的策略文件。在策略文件中,我们可以定义系统文件读取权限、序列化权限等。

​ 正常情况下,应用是没有启动安全管理器的,所以System.getSecurityManager() == null,直接跳过检查;

​ 根据Interruptible b 是否为null,分为两部分逻辑处理,但是都会调用本地方法interrupt0;下面分开说明两部分的逻辑。

Interruptible b != null

​ blocker为Thread的成员变量,并提供了blockedOn()方法设置blocker的值;

   void blockedOn(Interruptible b) {
        synchronized (blockerLock) {
            blocker = b;
        }
    }

​ 传统IO在读写时,虽然是阻塞的,但是无法被中断;NIO支持在读写操作时进行中断,channel若实现了InterruptiableChannel接口,则表示支持中断。如常用的FileChannel,SocketChannel,DatagramChannel等都实现了该接口。

​ 在其子类AbstractInterruptibleChannel中,定义了实现可中断IO机制的方法begin和end;NIO规定,在阻塞IO的语句前后,需要调用begin和end方法,为了保证end方法一定被调用,要求放在finally块中;

begin方法如下:

protected final void begin() {
        if (interruptor == null) {
            //初始化中断处理对象,中断处理对象中提供回调机制
            interruptor = new Interruptible() {
                    public void interrupt(Thread target) {
                        synchronized (closeLock) {
                            if (!open)
                                return;
                            //设置标志位
                            open = false;
                            interrupted = target;
                            try {
                                //关闭channel
                                AbstractInterruptibleChannel.this.implCloseChannel();
                            } catch (IOException x) { }
                        }
                    }};
        }
   	    //将中断处理对象注册到当前线程
        blockedOn(interruptor);
        Thread me = Thread.currentThread();
    	//当前线程如果被中断,则注册的中断处理对象可能没有被执行,手动触发一下
        if (me.isInterrupted())
            interruptor.interrupt(me);
    }

​ 总的来说,就是在Thread的中断逻辑中,挂载自定义的中断处理对象,这样Thread对象被中断时,会执行中断处理对象的回调,在回调中执行关闭channel操作,这样就实现了对线程中断的响应。

​ Thread添加中断处理逻辑 是依赖blockedOn方法,如下:

static void blockedOn(Interruptible intr) {         // package-private
        sun.misc.SharedSecrets.getJavaLangAccess().blockedOn(Thread.currentThread(),
                                                             intr);
    }

​ 这里用到了SharedSecrets类,通过SharedSecrets能够访问JDK类库中因为作用域的限制而无法访问的类或者方法(和反射实现的效果一样),该方法实际上就调用thread.blockedOn(interruptible)方法。

​ 回过来看看interrupt方法,在 b != null 时,首先调用interrupt0()方法,该方法只是设置interrupt标志位,然后调用b.interrupt方法,该方法就是上述说明的回调方法,一清二楚啦~

Interruptible b == null

​ 对于b==null时,会直接调用本地方法interrupt0(),该方法能够中断wait,sleep和join等阻塞等待的方法;

​ 对于java线程来说,最终都会映射为操作系统的线程。当执行interrupt()方法后,如果操作系统线程没有被中断,那么会设置操作系统线程的interrupt标志位为true,并唤醒线程的SleepEvent,随后唤醒线程的parker和ParkEvent。

​ ParkEvent包含了_ParkEvent变量和_SleepEvent变量,其中,_ParkEvent变量用于synchronized同步块和Object.wait()方法,_SleepEvent变量用于Thead.sleep();ParkEvent中包含了一把mutex互斥锁和一个cond条件变量,线程的阻塞和唤醒(park和unpark)通过它们实现的。

  • PlatformEvent::park() 方法会调用库函数pthread_cond_wait(_cond, _mutex)实现线程等待

    • synchronized块的进入和Object.wait()的线程等待都是通过PlatformEvent::park()方法实现
    • 注:Thread.join()是使用的Object.wait()实现的
  • PlatformEvent::park(jlong millis)方法会调用库函数pthread_cond_timedwait(_cond, _mutex, _abstime)实现计时条件等待

    • Thread.sleep(millis)就是通过PlatformEvent::park(jlong millis)实现
  • PlatformEvent::unpark()方法会调用库函数pthread_cond_signal (_cond)唤醒上述等待的条件变量

    • Thread.interrupt()就会触发其子类SleepEvent和ParkEvent的unpark()方法
    • synchronized块的退出也会触发unpark()。其所在对象ObjectMonitor维护了ParkEvent数组作为唤醒队列,synchronized同步块退出时,会触发ParkEvent::unpark()方法来唤醒等待进入同步块的线程,或等待在Object.wait()的线程。

​ 对于Synchronized等待事件,被唤醒后会尝试获取锁,如果失败则会通过循环继续park()等待,因此synchronized等待实际上不会被中断的;如果是Object.wait()事件,则会通过标记从而判断是否为notify()唤醒,如果不是则抛出InterruptedExcetion进行中断。

​ Parker与上述的ParkEvent类似,也持有一把mutex互斥锁和一个cond条件变量;凡是在java代码中通过unsafe.park()/unpark()的调用都会映射到Thread的_parker变量去执行。而unsafe.park()/unpark()正是由LockSupport类调用,如ReentrantLock,CountDownLatch,Semaphore,ThreadPoolExecutor,ArrayBlockingQueue等。(LockSupport.park()和unpark()类似于Object.wait和notify方法,不同的是,它不需要在同步代码块中,且unpark即便在park方法前进行调用,依然能够唤醒线程)

​ 并非所有的阻塞方法都抛出 InterruptedException。输入和输出流类会阻塞等待 I/O 完成,但是它们不抛出 InterruptedException,而且在被中断的情况下也不会提前返回。然而,对于套接字 I/O,如果一个线程关闭套接字,则那个套接字上的阻塞 I/O 操作将提前结束,并抛出一个 SocketExceptionjava.nio 中的非阻塞 I/O 类也不支持可中断 I/O,但是同样可以通过关闭通道或者请求 Selector 上的唤醒来取消阻塞操作。类似地,尝试获取一个内部锁的操作(进入一个 synchronized 块)是不能被中断的,但是 ReentrantLock 支持可中断的获取模式。

如何处理中断异常-InterruptedException

  1. 如果自己无法处理异常,可以在方法声明中向外抛出异常(throws InterruptedException),交给上层具体的业务来处理;
  2. 先捕获异常,做一些清理工作,然后再向外抛出异常;比如,玩家匹配游戏,当一个玩家匹配已经到来,但是另外一个玩家未到来,发生了中断,此时需要把已经到来的玩家重新放回队列中,然后再抛出异常;
public class PlayerMatcher {
    private PlayerSource players;
 
    public PlayerMatcher(PlayerSource players) { 
        this.players = players; 
    }
 
    public void matchPlayers() throws InterruptedException { 
        try {
             Player playerOne, playerTwo;
             while (true) {
                 playerOne = playerTwo = null;
                 // Wait for two players to arrive and start a new game
                 playerOne = players.waitForPlayer(); // could throw IE
                 playerTwo = players.waitForPlayer(); // could throw IE
                 startNewGame(playerOne, playerTwo);
             }
         }
         catch (InterruptedException e) {  
             // If we got one player and were interrupted, put that player back
             if (playerOne != null)
                 players.addFirst(playerOne);
             // Then propagate the exception
             throw e;
         }
    }
}
  1. 如果自己知道发生异常后如何进行处理,那么不应该向外抛出异常。比如当由Runnable定义的任务调用了一个可中断的方法时,那么当发生中断异常时是ok的,但是并不意味着捕获异常然后什么都不做,因为java中在检测到中断并抛出InterruptedException时,会清除中断状态(保证只能被中断一次);因此需要保留中断发生的证据,以便调用栈中更高层的代码能够知道中断,并对中断做出响应;可以通过interrupt()方法进行 重新中断 来完成。
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
 
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException e) { 
             // Restore the interrupted status
             Thread.currentThread().interrupt();
         }
    }
}
  1. 吞掉中断异常或者log一下,这也是最常见的处理方式,不推荐;
public class TaskRunner implements Runnable {
    private BlockingQueue<Task> queue;
 
    public TaskRunner(BlockingQueue<Task> queue) { 
        this.queue = queue; 
    }
 
    public void run() { 
        try {
             while (true) {
                 Task task = queue.take(10, TimeUnit.SECONDS);
                 task.execute();
             }
         }
         catch (InterruptedException swallowed) { 
             /* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
         }
    }
}
  1. 如果不能重新抛出 InterruptedException,不管您是否计划处理中断请求,仍然需要重新中断当前线程,因为一个中断请求可能有多个 “接收者”。标准线程池 (ThreadPoolExecutor)worker 线程实现负责中断,因此中断一个运行在线程池中的任务可以起到双重效果,一是取消任务,二是通知执行线程线程池正要关闭。如果任务生吞中断请求,则 worker 线程将不知道有一个被请求的中断,从而耽误应用程序或服务的关闭。
  2. 对于不可取消的任务,需要在合适的时候重新设置中断状态。有些任务拒绝被中断,这使得它们是不可取消的。但是,即使是不可取消的任务也应该尝试保留中断状态,以防在不可取消的任务结束之后,调用栈上更高层的代码需要对中断进行处理。清单 6 展示了一个方法,该方法等待一个阻塞队列,直到队列中出现一个可用项目,而不管它是否被中断。为了方便他人,它在结束后在一个 finally 块中恢复中断状态,以免剥夺中断请求的调用者的权利。(它不能在更早的时候恢复中断状态,因为那将导致无限循环 —— BlockingQueue.take() 将在入口处立即轮询中断状态,并且,如果发现中断状态集,就会抛出 InterruptedException。)
public Task getNextTask(BlockingQueue<Task> queue) {
    boolean interrupted = false;
    try {
        while (true) {
            try {
                return queue.take();
            } catch (InterruptedException e) {
                interrupted = true;
                // fall through and retry
            }
        }
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

Interrupted()和isInterrupted()

public static boolean interrupted() {
    return currentThread().isInterrupted(true);//本地方法,true清除中断状态
}
public boolean isInterrupted() {
    return isInterrupted(false); //本地方法,false不会清除中断状态
}

​ 两个方法都能查看线程的中断状态,区别在于,interrupted()是static方法,调用后会清除线程的中断状态;而isInterrupted()是普通方法,调用后不会清除线程的中断状态。

线程池优雅关闭

​ 该部分内容会在下个章节-优雅停机 部分进行讲解,敬请期待~

参考

Thread.interrupt相关源码分析

线程中断机制

java理论与实践:处理InterruptedException

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章