Java并发编程-如何终止线程

Posted 记忆力不好

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程-如何终止线程相关的知识,希望对你有一定的参考价值。

我们知道使用stop()、suspend()等方法在终止与恢复线程有弊端,会造成线程不安全,那么问题来了,应该如何正确终止与恢复线程呢?这里可以使用两种方法:

1.使用interrupt()中断方法。

2.使用volatile boolean变量进行控制。

在使用interrupt方法之前,有必要介绍一下中断以及与interrupt相关的方法。中断可以理解为线程的一个标志位属性,表示一个运行中的线程是否被其他线程进行了中断操作。这里提到了其他线程,所以可以认为中断是线程之间进行通信的一种方式,简单来说就是由其他线程通过执行interrupt方法对该线程打个招呼,让起中断标志位为true,从而实现中断线程执行的目的。

其他线程调用了interrupt方法后,该线程通过检查自身是否被中断进行响应,具体就是该线程需要调用isInterrupted方法进行判断是否被中断或者调用Thread类的静态方法interrupted对当前线程的中断标志位进行复位(变为false)。需要注意的是,如果该线程已经处于终结状态,即使该线程被中断过,那么调用isInterrupted方法返回仍然是false,表示没有被中断。

interrupt()方法的InterruptedException异常处理问题:

那么是不是线程调用了interrupt方法对该线程进行中断,该线程就会被中断呢?答案是否定的。因为Java虚拟机对会抛出InterruptedException异常的方法进行了特别处理:Java虚拟机会将该线程的中断标志位清除,然后跑出InterruptedException,这个时候调用isInterrupted方法返回的也是false

package com.rhwayfun.concurrency;

/**
 * Created by rhwayfun on 16-4-2.
 */
public class Interrupted {

    public static void main(String[] args){
        //创建一个休眠线程
        Thread sleepThread = new Thread(new SleepThread(),"SleepThread");
        //设为守护线程
        sleepThread.setDaemon(true);
        //创建一个忙线程
        Thread busyThread = new Thread(new BusyThread(),"BusyThread");
        //把该线程设为守护线程
        //守护线程只有当其他前台线程全部退出之后才会结束
        busyThread.setDaemon(true);
        //启动休眠线程
        sleepThread.start();
        //启动忙线程
        busyThread.start();
        //休眠5秒,让两个线程充分运行
        SleepUtil.second(5);
        //尝试中断线程
        //只需要调用interrupt方法
        sleepThread.interrupt();
        busyThread.interrupt();
        //查看这两个线程是否被中断了
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
        //防止sleepThread和busyThread立刻退出
        SleepUtil.second(2);
    }

    /**
     * 不断休眠
     */
    static class SleepThread implements Runnable{
        public void run() {
            while (true){
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 不断等待
     */
    static class BusyThread implements Runnable{
        public void run() {
            while (true){
                //忙等待
            }
        }
    }
}



执行结果:

这里写图片描述

可以发现内部不停睡眠的方法执行执行中断后,其中断标志位返回的是false,而一直运行的线程的中断标志位则为true。这里主要由于Sleep方法会抛出InterruptedException异常,在处理InterruptedException异常时Java虚拟机把SleepThread的中断标志位复位了,所以才会显示false

Thread.interrupt()方法不会中断一个正在运行的线程。这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异(InterruptedException),从而提早地终结被阻塞状态。 


使用volatile boolean变量进行控制的问题:

     在上面代码中,在代码中调用cancel方法来取消i的自增请求,如果Runner线程在下次执行,或者正要执行下一次自增请求时判断on的时是否变为了false,如果是则终止执行。

      根据运行结果,Runner的计数任务最终会被取消,然后退出。在Runner线程最终取消执行之前,会有一定的时间,如果在在这个时间内,调用此方法的任务调用了一个会阻塞的方法,比如BlockingQueue的put方法,那么可能该任务一直违法检测到on的值变为false,因而Runner线程不会终止。

一个例子

比如下面的代码就说明了这一点:

import java.math.BigInteger;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * Created by rhwayfun on 16-4-11.
 */
public class BrokenShutdownThread extends Thread {

    //是否继续运行的标志
    private static volatile boolean on = true;
    //阻塞队列
    private final BlockingQueue<BigInteger> queue;

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

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (on) {
                //生产者一次可以放40个数
                for (int i = 0; i < 40; i++){
                    queue.put(p = p.nextProbablePrime());
                    System.out.println(Thread.currentThread().getName() + ": put value " + p);
                }
            }
        } catch (InterruptedException e) {}
    }

    public void cancel() {
        on = false;
    }

    /**
     * 消费者线程
     */
    static class Consumer extends Thread{
        //阻塞队列
        private final BlockingQueue<BigInteger> queue;

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

        @Override
        public void run() {
            try {
                while (on) {
                    //消费者一次只能消费1个数
                    System.out.println(Thread.currentThread().getName() + ": get value " + queue.take());
                }
                System.out.println("work done!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<BigInteger> queue = new LinkedBlockingQueue<>(5);
        BrokenShutdownThread producer = new BrokenShutdownThread(queue);
        //启动计数线程
        producer.start();
        TimeUnit.SECONDS.sleep(1);
        new Consumer(queue).start();
        TimeUnit.SECONDS.sleep(1);
        producer.cancel();
    }

运行上面的程序,发现虽然控制台输出了work done!的信息,但是程序仍然没有停止,仔细分析就会发现生产者的速度(40个数/次)远大于消费者的速度(1个数/次),造成队列被填满,put方法被阻塞。虽然在运行一秒后调用cancel方法将volatile变量on设为了false,但是由于生产者线程的put方法被阻塞,所以无法从阻塞的put方法中恢复,自然程序就无法终止了。


对上面代码的修正

根据以上的分析只需要对代码做如下的修改就能正确终止线程:

public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (on && !Thread.currentThread().isInterrupted()) {
                //生产者一次可以放40个数
                for (int i = 0; i < 40; i++){
                    queue.put(p = p.nextProbablePrime());
                    System.out.println(Thread.currentThread().getName() + ": put value " + p);
                }
            }
        } catch (InterruptedException e) {
            //让线程退出
            return;
        }
    }

    public void cancel() {
        on = false;
        interrupt();
    }
static class Consumer extends Thread{
        //阻塞队列
        private final BlockingQueue<BigInteger> queue;

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

        @Override
        public void run() {
            try {
                while (on && !Thread.currentThread().isInterrupted()) {
                    //消费者一次只能消费1个数System.out.println(Thread.currentThread().getName() + ": get value " + queue.take());
                }
                System.out.println("work done!");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }



在计数线程中通过使用一个boolean变量成功终止了线程。这种通过标志位或者中断操作的方式能够使得线程在终止的时候有机会去清理资源,而不是武断地将线程终止,因此这种终止线程的做法更优雅和安全。



以上是关于Java并发编程-如何终止线程的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程:线程挂起恢复与终止的正确方法(含代码)

Day854.两阶段终止模式 -Java 并发编程实战

Day854.两阶段终止模式 -Java 并发编程实战

转: Java并发编程之三:线程挂起恢复与终止的正确方法(含代码)

Java并发编程:线程挂起恢复与终止

Java并发编程的艺术读书笔记——Java并发编程基础