如何优雅的"停止"一个正在运行的JAVA线程

最近在开发过程中,遇到一个需求

  • 当用户输完快递单号后,发起请求,自动补全手机号码
  • 请求超过5秒后结束请求,并提示用户没找到手机号

第一发

很简单的需求,是吧。然后我就啪啪啪写下了代码。

public void request() {

    thread = new Thread(new Runnable() {
        @Override
        public void run() {
            // 请求网络
            // ...
            // ...
            // 返回结果请求To UI
        }
    });

    thread.start();

    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            thread.stop();
        }
    }, 5 * 1000);

}

看起来是这么的完美。不过这个stop上面有一根删除线是什么鬼。它的文档是这样描述的。

Requests the receiver Thread to stop and throw ThreadDeath. The Thread is resumed if it was suspended and awakened if it was sleeping, so that it can proceed to throw ThreadDeath.
@deprecated
because stopping a thread in this manner is unsafe and can leave your application and the VM in an unpredictable state.

大概意思就是,调用这个方法会抛出一个ThreadDeath的异常。用这个方法去停止一个线程是不安全的,它会让你的应用和VM处于一个不可预知的状态。

第二发

stop()方法是不安全的,故不再使用,在api中还有一个interrupt()方法。
我们接下来看看这个interrupt()是怎么个用法。

  • 如果该线程正阻塞于Object类的wait()、wait(long)、wait(long, int)方法,或者Thread类的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)方法,则该线程的中断状态将被清除,并收到一个java.lang.InterruptedException。
  • 如果该线程正阻塞于interruptible channel上的I/O操作,则该通道将被关闭,同时该线程的中断状态被设置,并收到一个java.nio.channels.ClosedByInterruptException。
  • 如果该线程正阻塞于一个java.nio.channels.Selector操作,则该线程的中断状态被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用java.nio.channels.Selector.wakeup()方法一样。
  • 如果上述条件都不成立,则该线程的中断状态将被设置。

从这个描述中,可以看出,interrupt()并不会直接的去停止线程,而是去设置一个中断状态,也就是打了一个标记,系统在合适的时候会去停止这个线程。

提到interrupt(),有两个方法就不得不说。

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

检测当前线程是否中断,是就返回true,否则false,并清除中断状态(就是把中断标志设置为false)

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

检测当前线程是否已经中断,是则返回true,否则false。中断状态不受该方法的影响。如果中断调用时线程已经不处于活动状态,则返回false。

这两个方法长得有点像,故使用的时候要注意一下子。

这么看来,一个正在运行的线程是不能立即被安全的停止,当然这是对于机器来说,但是我们可以控制这个线程在某时间停止或者说在某种条件下停止。

那么新版的代码来了。

public void request() {

    thread = new Thread(new Runnable() {
        @Override
        public void run() {
            // 请求网络
            // ...
            // ...
            if (thread != null && !thread.isInterrupted()) {
                // 返回结果请求To UI
            }
        }
    });

    thread.start();

    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            if (thread != null && thread.isAlive()) {
                thread.interrupt();
            }
        }
    }, 5 * 1000);

}

看上去很不错。当我在review code的时候,发现了一些问题。
举个栗子
第一个请求过来了,用时2秒,这个时候我们的handler还在跑。第4秒,又来了一个请求,第5秒,handler的时间到了,此刻的thread指向于新的一次请求,于是我们的一个完美请求就被中断掉了。

第三发

当然这也是有解决办法的,我们在第二次请求的时候,首先把前一次的定时任务给取消掉。基于这个需求,我们可以使用Timer,ScheduledExecutorService,HandlerThread,包括第二发中的Handler都是可以的。
经过网上的东看看西逛逛,最终选择ScheduledExecutorService作为这个定时器。

最终版代码

private QueryThread queryThread;
private ScheduledExecutorService executor;

public void request() {
    if (queryThread != null && queryThread.isAlive()) {
        queryThread.interrupt();
        executor.shutdownNow();
    }
    queryThread = new QueryThread();
    queryThread.start();

    executor = Executors.newScheduledThreadPool(1);
    executor.schedule(new Runnable() {
        @Override
        public void run() {
            if (!queryThread.isInterrupted()) {
                queryThread.interrupt();
                // 告知UI
            }
        }
    }, 5 * 1000, TimeUnit.MILLISECONDS);
}

class QueryThread extends Thread {
    @Override
    public void run() {
        // 请求网络
        // ...
        // ...
        if (!isInterrupted()) {
            // 返回结果请求UI
        }
    }
}

第四发

这次来谈谈最佳实践(这部分是引用的)

public class BestPractice extends Thread {
    private volatile boolean finished = false;   // ① volatile条件变量
    public void stopMe() {
        finished = true;    // ② 发出停止信号
    }
    @Override
    public void run() {
        while (!finished) {    // ③ 检测条件变量
            // do dirty work   // ④业务代码
        }
    }
}

当④处的代码阻塞于wait()或sleep()时,线程不能立刻检测到条件变量。因此②处的代码最好同时调用interrupt()方法。

参考文献

如何停止一个正在运行的java线程