如何优雅地关闭一个线程

如何优雅地关闭一个线程

前面给大家展示了如何创建和使用Java线程,讲解了线程的状态及其每个状态之间的流转,接下来给大家分享,如何正确地关闭一个Java线程。

我们知道,一个Java线程被启动后,线程类中的run()方法会被回调,当run()方法执行完成后进入终止状态,这是线程的自然终止。当我们不想让线程自然终止,想在其运行期间就关闭,应该怎么做呢?

Thread.stop()

Thread中提供了一个stop()方法,用于关闭正在运行中的线程。大家可以去看看这个方法的源码,可以看到它被@Deprecated注解标识,意味着此方法是不推荐使用的。大家可以看看文档说明👇

https://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html

简单说明一下,该方法会导致两个问题

  • 在执行run方法时,每一个行代码都可能会抛出 ThreadDeath 异常
  • 会释放当前线程中的所有锁,释放之后可能会让一些操作失去原子性。

下面可以看看示例代码

public class CloseThreadEample implements Runnable {

    @Override
    public void run() {
        try {
            for (int i = 0; i < 1000 ;i++ ) {
                System.out.println("HelloWorld" + i);
            }
            System.out.println("over");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new CloseThreadEample());
        thread.start();
        Thread.sleep(1);
        thread.stop();
    }
}

运行结果:

HelloWorld52
HelloWorld53java.lang.ThreadDeath
at java.lang.Thread.stop(Thread.java:853)
at org.example.closethread.CloseThreadEample.main(CloseThreadEample.java:25)

这段运行结果已经展示了前面提到的两个问题,第一个问题可以明显看到,使用stop后,会抛出ThreadDeath异常,让run方法被中断,从而无法正常打印出 “over”,那第二个问题是怎么体现出来的呢?我们可以继续看看System.out.println方法的源码

 public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

println方法是使用了synchronized关键字,理论上print(x)newLine()两行指令是原子性的,也就是说要么一起执行成功,要么都不执行,从运行结果可以看到,打印完“HelloWorld53”后,没有继续打印换行操作,就直接打印了异常信息,这就说明了,使用stop方法后,释放了synchronized同步锁,使得用于打印的两行指令代码失去了原子性,从而导致了newLine()没有执行。既然stop()方法不推荐使用,那还有什么方法关闭线程呢?

Thread.interrupt()

Thread提供了interrupt()方法,它没有像stop()方法那样,直接中断一个线程的执行,而是通过信号量来通知线程可以停止执行了,当线程收到该信号量时,可以通过isInterrupted()方法判断当前是否需要中断。这样的停止方式,相比stop()是不是更加友好,让线程自己判断能否中断,从而保证业务处理的完整性。

下面看看具体例子:

public class InterruptThreadExample implements Runnable {
    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("helloworld" + i++);
        }
        System.out.println("over");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptThreadExample());
        thread.start();
        thread.sleep(10);
        thread.interrupt();
    }
}

运行结果:

helloworld260
helloworld261
helloworld262
over

在示例代码中,通过isInterrupted()方法判断线程是否被中断,如果不是,则循环打印“helloworld”字符串。可以看到,使用interrupt()方法后,线程是正常结束的,“over” 也是正常打印。

当然,虽然这种方式可以友好地停止线程,但是,如果使用不当,线程是无法被停止的。

当一个线程因为sleep(),Object.wait()等方法进入阻塞状态时,此时调用interrupt()方法,线程是无法正常响应的,它会抛出一个InterruptedException异常,这个异常我们可以在各个阻塞方法中看到。

   public static void sleep(long millis, int nanos) throws InterruptedException {...}
   
    public static native void sleep(long millis) throws InterruptedException;
   
   public final native void wait(long timeout) throws InterruptedException;
   
   public final void join() throws InterruptedException {...}
  
   ...
    

抛出这个异常有什么用呢?下面我们继续看例子

public class InterruptThreadExample implements Runnable {
    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("helloworld" + i++);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("over");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptThreadExample());
        thread.start();
        thread.sleep(1000);
        thread.interrupt();
    }
}

运行结果:

helloworld6
helloworld7
helloworld8
helloworld9
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.closethread.InterruptThreadExample.run(InterruptThreadExample.java:14)
at java.lang.Thread.run(Thread.java:748)
helloworld10
helloworld11
helloworld12

笔者在原来的代码上继续加上了Thread.sleep(100)代码,从运行结果可以看到,当调用interrupt()方法后,会抛出异常,但是线程仍然在运行,持续打印“helloworld”,这是什么原因造成的呢?笔者也不卖关子了,继续看下一个例子。

public class InterruptThreadExample implements Runnable {
    @Override
    public void run() {
        int i = 0;
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("helloworld" + i++);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                System.out.println("出现异常后线程的中断状态: " + Thread.currentThread().isInterrupted());
                Thread.currentThread().interrupt();
                System.out.println("重新调用interrupt()方法后的线程状态:" + Thread.currentThread().isInterrupted());
                e.printStackTrace();
            }
        }
        System.out.println("over");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptThreadExample());
        thread.start();
        thread.sleep(1000);
        thread.interrupt();
        System.out.println("调用interrpt()方法后的线程状态:" + thread.isInterrupted()); 
    }
}

运行结果:

helloworld8
helloworld9
调用interrpt()方法后的线程状态:true
出现异常后线程的中断状态: false
重新调用interrupt()方法后的线程状态:true
over
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at org.example.closethread.InterruptThreadExample.run(InterruptThreadExample.java:14)
at java.lang.Thread.run(Thread.java:748)

在这个例子中,笔者分别在调用interrupt()方法后和出现异常后打印了线程的isInterrupted()信息,从运行结果可以看到,线程是被正常关闭的,一开始在main方法中调用了interrupt()方法后,可以看到isInterrupted()是为ture的,但是在出现异常之后,isInterrupted()又变回了false,这是因为在抛出异常时,除了唤醒被阻塞的线程外,还会对中断标记进行复位,将中断标记重新设置为false,此时需要重新调用interrupt()方法才能中断线程。

这样设计的目的是为了将中断决定权交给线程本身,如果抛出异常,提醒当前线程有中断的操作,至于接下来怎么处理,就取决于线程本身如何决定了,可以不用理会此次中断操作继续向下执行,也可以通过break中断死循环,或者通过return直接返回等等。

我们可以看看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();
}
    private native void interrupt0();

可以看到, 在该方法中,通过调用interrupt0()方法设置一个标记,而此方法又是native方法,它是使用C/C++语言实现的接口,设置标记的逻辑是在JVM层面实现的。

接下来继续看看 isInterrupted()方法的源码


    /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    
    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if this thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see     #interrupted()
     * @revised 6.0
     */

    public boolean isInterrupted() {
        return isInterrupted(false);
    }

    /**
     * Tests if some Thread has been interrupted.  The interrupted state
     * is reset or not based on the value of ClearInterrupted that is
     * passed.
     */

    private native boolean isInterrupted(boolean ClearInterrupted);

同样,最终也是调用一个native方法去判断这个标识,从native方法的注释上可以解读到,它是通过参数ClearInterrupted判断是否需要重置中断状态的,而传入true的的地方是public static boolean interrupted()这个方法,我们看一下这个方法会在哪里被用到。

如何优雅地关闭一个线程

大部分都是在JUC包中用到,我们先随便挑个方法进去看一下,这里选择LinkedTransferQueue类的transfer方法

public void transfer(E e) throws InterruptedException {
        if (xfer(e, true, SYNC, 0) != null) {
            Thread.interrupted(); // failure possible only due to interrupt
            throw new InterruptedException();
        }
    }

可以看到,它在调用Thread.interrupted();后,对标记位进行重置,然后抛出了InterruptedException异常,有没有觉得这里的设计和线程阻塞后,然后调用interrupt()方法进行中断时抛出InterruptedException异常是一样的,从这里也可以联想推测JVM底层阻塞方法的实现,应该也是先将线程的中断标记位重新设置为false,然后抛出异常。

因为笔者没有接触过C/C++,所以无法继续给大家剖析JVM底层的代码实现,只能通过在Java层面找到类似的设计进行类比,语言虽不同,但思想是相通的。

总结

今天主要讲了在Java中关闭一个线程的两种方式,

  • 第一种是通过stop()方法强制中断,此方法不推荐使用,会出现不可控的问题;
  • 第二种是通过interrupt()方法,通过设置标记位进行中断,也是Java中推荐的方式,将中断选择权交给了线程,这里需要注意的是抛出InterruptedException这个异常不意味着线程的结束,如果你使用到了线程的中断标识,记住它会对中断标识进行复位,当然你可以自己捕获到异常后进行自己想要的一些处理。

好了,今天的学习就到这里。读完记得 一个,最新文章可以点击 阅读原文


原文始发于微信公众号(DevUnion):如何优雅地关闭一个线程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/35459.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!