首先要明确的是Java没有提供任何机制来安全的终止线程。

Java虽然提供了stop方法来终止线程,但是这种方式简单粗暴,很可能造成数据不一致的情况,因此stop方法已经弃用了。

目前较为安全地终止线程方式有两种:

1)使用标志位,让线程run方法在合适的时候结束执行,从而终止线程。

2)使用中断机制,让线程run方法在中断的状态下结束执行,从而终止线程。

ps:这两种方案虽然都可以安全的终止线程,但是使用标志位往往具有局限性,譬如使用阻塞容器的线程,在容器阻塞的情况下,往往没有机会检测到到标志位,从而导致线程无法终止(后面会有实例对比来说明这个问题)。所以,就有下面的结论:

通常,使用中断机制是实现线程终止的最合理方式。

一、使用标志位终止线程

如下实例声明了一个volatile类型的boolean变量作为标志位,表示线程是否终止,当标志位为false时线程才保持运行。

/**
     * 通过标志位来停止线程
     */
    static class CancelThreadWorker extends Thread {
        private volatile boolean cancelled = false;

        public CancelThreadWorker(String name) {
            super(name);
        }

        public void run() {
            int i = 0;
            while (!cancelled) {
                try {
                    System.out.println(i);
                    Thread.sleep(2000);
                    i++;
                } catch (InterruptedException e) {
                    ExceptionUtil.printException(e);
                }
            }
        }

        public void cancel() {
            cancelled = true;
        }
    }

ps:为了后期测试方便,写个简单的工具类MyThreadUtil对基础打印操作做了封装,如下:

static class MyThreadUtil {
        public static void printException(Exception e) {
            System.out.println(Thread.currentThread().getName() + " throws an Exception:" + e);
        }
        public static void printState(Thread thread) {
            System.out.println(thread.getName() + " current state is:" + thread.getState());
        }
    }

main方法测试代码如下:

CancelThreadWorker cancelThreadWorker = new CancelThreadWorker("cancelThreadWorker");
        cancelThreadWorker.start();
        try {
            Thread.sleep(2500);
        } catch (InterruptedException e) {
            ExceptionUtil.printException(e);
        }
        cancelThreadWorker.cancel();
        while (cancelThreadWorker.isAlive()) {} //阻塞一小段时间,等待线程终止
        System.out.println(cancelThreadWorker.getState());

控制台输出信息如下:

0
1
cancelThreadWorker current state is:TERMINATED

可以看出使用标志位确实让线程终止了。

二、使用中断机制

中断是一种协作机制,调用interrupt方法并不代表线程立刻就停止了,而是告诉线程你应该停止了。

Java中设计一个良好的api通常要考虑对中断做出及时响应,方便调用者知道被中断的线程接收到了中断信息,从而可以提前结束线程。例如Thread.sleep,Object.wait,Thread.join,阻塞容器的take和put,这类方法之所以被设计成抛出中断异常InterruptedException,就是为了对中断做出及时响应,告诉调用者,我已经收到中断信号了!值得注意的是,这类库方法在抛出InterruptedException的时候会清除线程的中断状态。

如下的实例演示了如何通过中断机制来终止线程,通过判断当前线程中断状态是否为false,仅当中断状态为false时run方法才执行。

当外部线程调用了该线程的interrupt方法时,sleep方法会对中断做出响应,抛出InterruptedException,继而重置中断状态为false。由于run方法限制了异常只能捕获,所以我们必须对异常及时处理,这里处理方案有两种,1)重新调用当前线程的interrupt方法让中断状态恢复为true。2)直接使用break退出循环即可

static class InterruptThreadWorker extends Thread {
        public InterruptThreadWorker(String name) {
            super(name);
        }

        public void run() {
            int i = 0;
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    System.out.println(i);
                    Thread.sleep(2000);
                    i++;
                } catch (InterruptedException e) {
                    MyThreadUtil.printException(e);
                    Thread.currentThread().interrupt(); //由于中断异常会清除中断标记,所以此处一定要重新调用中断方法。当然也可以用break来代替
//                    break; //使用break代替上面的interrupt也可以
                }
            }
        }
    }

main方法测试代码如下:

InterruptThreadWorker interruptThreadWorker = new InterruptThreadWorker("interruptThreadWorker");
        interruptThreadWorker.start();
        try {
            Thread.sleep(2500);
        } catch (InterruptedException e) {
            MyThreadUtil.printException(e);
        }
        interruptThreadWorker.interrupt(); //传递中断信号,告诉interruptThreadWorker,你该终止了哈
        while (interruptThreadWorker.isAlive()) {} //阻塞一小段时间,等待线程终止
        MyThreadUtil.printState(interruptThreadWorker);

控制台输出如下:

0
1
interruptThreadWorker throws an Exception:java.lang.InterruptedException: sleep interrupted
interruptThreadWorker current state is:TERMINATED

可以看出在这个案例和上一个案例的演示结果是一样的。

三、使用标志位方案终止线程的局限性

前面提到过标志位的终止方案对于阻塞容器来说可能会失效,如下是一个具体的案例,源于<Java并发编程实战>这本书。

/**
     * 素数生产者线程
     */
    static class PrimeProducer extends Thread {

        private final BlockingQueue<BigInteger> queue;
        private volatile boolean cancelled = false;

        public PrimeProducer(BlockingQueue<BigInteger> queue) {
            this.queue = queue;
        }

        public void run() {
            BigInteger p = BigInteger.ONE.ONE;
            while (!cancelled) {
                try {
                    queue.put(p = p.nextProbablePrime());
                } catch (InterruptedException e) {
                    MyThreadUtil.printException(e);
                }
            }
        }

        public void cancel() {
            cancelled = true;
        }
    }

素数生产者线程PrimeProducer维护了一个阻塞容器BlockingQueue,run方法使用while循环判断标志位cancelled是否为false,如果满足则向容器中添加素数。

main方法的测试代码如下,我们设置阻塞队列的长度为9,主线程睡眠20ms之后,设置标志位为false。

BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9);
        PrimeProducer brokenPrimeProducer = new PrimeProducer(primes);
        brokenPrimeProducer.start();
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            MyThreadUtil.printException(e);
        }
        brokenPrimeProducer.cancel();
        System.out.println(brokenPrimeProducer.queue.size());
        while (brokenPrimeProducer.isAlive()) {} //阻塞一小段时间,等待线程终止
        MyThreadUtil.printState(brokenPrimeProducer);

控制台打印如下:

9

细心观察,发现控制台其实一直处于运行状态,并没有打印这个线程的状态,也就是说线程并没有被终止,而是一直处于阻塞状态。

这是因为:线程开启的20ms之后,阻塞队列已经被填满,而put作为一个阻塞方法(类似的take),如果队列满则会持续到阻塞到队列有可用空间为止。本例中只有生成者线程而没有消费者线程,导致put方法持续阻塞,所以无法检测到while循环标志位的状态,故线程无法终止。

如果我们将main方法中的阻塞队列扩大,线程会被正常终止的。只需修改下面一行:

BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9999);

或者:你也可以保持队列容量不变,将主线程的睡眠时间缩小:

Thread.sleep(2);

采用第一种实验方案,控制台打印如下:

56
Thread-0 current state is:TERMINATED

可以看出阻塞队列在添加完第56个元素之后,发现标志位为true,不再添加元素,线程终止。证明前面的分析完全成立。

显然实际项目开发中,我们不可能为了终止线程来刻意改变阻塞队列大小,这是一种投机行为,包含了运气成分,它可能会影响项目质量。所以对于包含阻塞队列的线程来说,不提倡使用标志位的策略终止线程。

本案例中对于素数生产者线程来说,使用中断是最好的策略,将PrimeProducer改造成以下形式:

/**
     * 素数生产者线程
     */
    static class PrimeProducer extends Thread {

        private final BlockingQueue<BigInteger> queue;

        public PrimeProducer(BlockingQueue<BigInteger> queue) {
            this.queue = queue;
        }

        public void run() {
            BigInteger p = BigInteger.ONE.ONE;
            while (!isInterrupted()) {
                try {
                    queue.put(p = p.nextProbablePrime());
                } catch (InterruptedException e) {
                    MyThreadUtil.printException(e);
                    cancel();
                }
            }
        }

        public void cancel() {
            this.interrupt();
        }
    }

依旧设置阻塞队列大小为9,延时20ms,main方法运行效果如下,可以看出控制台打印之后正常结束,线程不再阻塞。

9
Thread-0 throws an Exception:java.lang.InterruptedException
Thread-0 current state is:TERMINATED

四、stop方法不安全的体现

stop方法不适合用来终止线程,是因为它是强行结束线程,如果对象是一种不连贯的状态,即使方法是同步的,也会立刻释放已持有的锁,从而造成数据不一致。

如下的案例演示这个问题:

static class UnstableObject {
        private String first = "ja";
        private String second = "va";

        public synchronized void setValues(String first, String second) throws Exception{
            this.first = first;
            Thread.sleep(10000);
            this.second = second;
        }

        public String getFirst() {
            return first;
        }

        public void setFirst(String first) {
            this.first = first;
        }

        public String getSecond() {
            return second;
        }

        public void setSecond(String second) {
            this.second = second;
        }
    }

UntableObject这个类维护了两个属性first和second,并提供了一个同步方法setValues对两个属性分别赋值,然而赋值过程并不是连贯的,存在一个睡眠10s的过程,如果一个线程调用setValues并在10s内进行被stop,则只会对第一个属性成功赋值,第二个属性仍然保持默认。

我们测试代码如下:

UnstableObject unstableObject = new UnstableObject();
        Thread thread = new Thread(()->{
            try {
                unstableObject.setValues("p", "hp");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        thread.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            MyThreadUtil.printException(e);
        }
        thread.stop();
        MyThreadUtil.printState(thread);
        System.out.println(unstableObject.getFirst() +  unstableObject.getSecond());

控制台输出如下:

Thread-0 current state is:TERMINATED
pva

可以看出线程虽然终止了,但我们渴望输出php,它却输出了pva,这就是stop不安全的一个体现。