并发基础之正确停止多线程

2019-11-07 16:02:04来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

并发基础之正确停止多线程

原理介绍

使用interrupt来通知,而不是强制。

在JAVA中我们启动一个线程很容易的,但是当我们停止一个线程并不是直接立刻马上就可以上这个线程停止,JAVA为我们提供了interrupt这个方法,简单来说这个方法的作用就是给当前运行的线程加上一个标志位,表示当前这个线程可以停止了,但是这个线程具体什么时候停止不是我们能够说了算的,而是由线程本身决定。

如何正确停止线程

1)先来看一个通常的情况,如果一个线程运行到一半,我们想让它停止运行,该怎么做:

/**
 * @author Chen
 * @Description 通常情况下停止一个多线程
 * @create 2019-11-06 20:45
 */
public class StopThreadWithoutSleep  implements Runnable{
    /**
     * 打印所有10000的倍数,上限是最大整数的一半
     */
    @Override
    public void run() {
        int num = 0;
        while (!Thread.currentThread().isInterrupted()&&num<=Integer.MAX_VALUE/2){
            if (num%10000 == 0){
                System.out.println(num+"是10000的倍数");
            }
            num++;

        }
        System.out.println("任务运行结束了");

    }

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

这里让线程休眠1s然后调用了interrupt()告诉线程可以停止了,然后在线程运行时会进行检,测如果!Thread.currentThread().isInterrupted()才执行下面的程序,这样我们的多线程程序就顺利的停止了。

2)如果线程阻塞了,会影响我们停止线程的方式,下面我们来看一下在阻塞的情况下线程该如何停止:

/**
 * @author Chen
 * @Description 有阻塞的情况下停止线程
 * @create 2019-11-06 20:58
 */
public class StopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            try {
                int num = 0;
                while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        System.out.println(num+"是100的倍数");
                    }
                    num++;
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(500);
        thread.interrupt();
    }
}

运行结果:

image.png

这个例子是打印300以内100的倍数,打印完之后休眠1000ms,然后在线程运行500ms后停止线程,我们知道这时候线程应该是在休眠状态的,在调用这种能然线程阻塞方法时候(如sleep,wait)如果遇到停止线程,此时我们catch了异常,此时就可以做到当程序进入阻塞过程中,依然可以响应这个中断。

3)接下来我们在来看下一种情况:如果线程每次循环后都阻塞,这种情况怎么解决?

/**
 * @author Chen
 * @Description 每次循环都调用sleep或wait等方法,
 * 这种情况不需要每次迭代都判断是否已经中断
 * @create 2019-11-06 20:58
 */
public class StopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            try {
                int num = 0;
                while (num <= 10000) {
                    if (num % 100 == 0) {
                        System.out.println(num+"是100的倍数");
                    }
                    num++;
                    Thread.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

这个例子是打印10000以内100倍数,每次循环后线程都休眠10ms,线程启动5000ms后进行中断,此时我我们可以思考一下,其实我们程序在执行每次循环中的方法是很快的,大多数时间都是在休眠10ms的这里,这时候当我们调用interrupt(),线程就可以很快的响应中断,而不需要我们每次都在循环开始的时候取判断中断标志位。

4)注意:while内放try catch 会导致中断失效的情况

/**
 * @author Chen
 * @Description while里面放try/catch会导致 中断失效的情况
 * @create 2019-11-06 21:51
 */
public class CannotInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            while (num <=10000 && !Thread.currentThread().isInterrupted()){
                if (num%100== 0){
                    System.out.println(num+"是100的倍数");
                }
                num++;
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

运行结果:

image8a962a554c48d23b.png

这个例子和上面的例子是差不多的,同样是打印10000以内100倍数,每次循环后线程都休眠10ms,线程启动5000ms后进行中断,不同的是本次把try/catch语句放在了循环里面,按照我们上面所学的想象的结果应该是打印出异常信息后线程就停止了,但是实际情况并不是这样。下面来总结一下原因:

因为我们在循环内部已经已经做出了响应,也就是打印出异常信息,此时线程会继续向下执行,进入下一次循环。而我们在下一次循环时候也加了判断标志位的代码,依然没有起到作用,这是因为JAVA在设计sleep的时候有一个理念:当它一旦响应中断后就会把这个标志位清除。

5)最佳实践之优先抛出

/**
 * @author Chen
 * @Description 最佳实践:catch了InterruptedExcetion之后的优先选择:在方法签名中抛出异常 那么在run()就会强制try/catch
 * @create 2019-11-06 22:23
 */
public class RightWayStopThreadInProd1 implements  Runnable {
    @Override
    public void run() {
        while (true && !Thread.currentThread().isInterrupted()){
            System.out.println("go");
            try {
                throwInMethod();
            } catch (InterruptedException e) {
                //保存日志、停止程序
                System.out.println("保存日志");
                e.printStackTrace();
            }
        }
    }
    private void throwInMethod() throws InterruptedException {
        //如果此时不是抛出而是try/catch此中断就会被吞掉
        Thread.sleep(2000);
    }

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

这里我们需要注意的是如果我们在run方法中做了很多业务逻辑,而run方法中又调用了其他的方法。那么如果其他的方法中发生了阻塞,而阻塞后如果直接try/catch就会使得我们的run方法内没有感知,就不能做相应的处理等。此时run()内部的方法正确的做法应该是把异常向上抛出或者是使用接下来介绍的恢复中断的方法重新设置标志位

6)最佳实践之恢复中断

/**
 * @author Chen
 * @Description 最佳实践:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,
 * 以便于在后续的执行中,依然能够检查到刚才发生了中断
 * @create 2019-11-06 22:23
 */
public class RightWayStopThreadInProd2 implements  Runnable {
    @Override
    public void run() {
        while (true ){
            if (Thread.currentThread().isInterrupted()){
                System.out.println("程序运行结束");
                break;
            }
            System.out.println("go");
            throwInMethod();
        }
    }
    private void catchMethod() {
        //如果此时不是抛出而是try/catch此中断就会被吞掉
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }

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

这个例子在run()内调用的方法中try/catch后又重新设置了标志位,也就是如果在catchMethod()检测到了遇到中断会抛出异常进行响应中断,然后把中断标志位恢复,把这个异常捕获时候我们做了处理:Thread.currentThread().interrupt()重新设置标志位后run方法内就能够正常的中断线程了。

6)能够响应中断的方法总结:

Object.wait()

Thraed.sleep()

Thread.join()

java.util.concurrent.BlockingQueue.take()/put()

java.util.concurrent.locks.Lock.lockInterruptibly()

java.util.concurrent.CountDownLatch.await()

java.util.concurrent.CyclicBarrier.await()

java.util.concurrent.Exchanger.exchange()

java.nio.channels.InterruptibleChannel相关方法

java.nio.channels.Selector的相关方法

错误的停止方法

1)被弃用的stop,suspend和resume方法

使用stop停止线程会使线程马上停止,不能保证一个单位内所有的任务都执行完成。如果有是一个转账操作,而这个操作需要子线程执行多个转账,那么如果子线程执行到一半就停止了会带来很严重的后果。

线程挂起 (suspend)和继续执行(resume),这两个 操作是一对相反的操作 ,被挂起的线程,必须要等到resume()操作后,才能继续执行。不推荐使用suspend(),是因为suspend()在导致线程暂停的同时,并不会去释放任何锁资源。此时,其他任何线程想要访问被它暂用的锁时,都会被牵连,导致无法正常继续运行,很有可能导致死锁。

2)使用volatile设置boolean设置标记位

volatile可以简单的理解为是解决变量在多个线程之间的可见性问题的一个关键字,加了volatile的变量线程都可以访问到这个变量的值。

下面先来看一下这种方式具体如何使用:

思路就是自己设置一个中断标志位,在循环开始的时候检查这个标志位,想要停止线程的时候把中断标志位设置为true。

/**
 * @author Chen
 * @Description 使用Volatile停止多线程例子
 * @create 2019-11-07 18:53
 */
public class UserVolatileInterrupt implements Runnable{
    //设置中断标记为false
    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 10;
        try {
            while (num <= 100000&& !canceled){
                if (num%100 == 0){
                    System.out.println(num + "是100的倍数。");
                }
                num++;
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) throws InterruptedException {
        UserVolatileInterrupt v = new UserVolatileInterrupt();
        Thread thread = new Thread(v);
        thread.start();
        Thread.sleep(5000);
        v.canceled = true;
    }
}

运行结果:

image.png

这种方式看起来可行,但有些时候却无法正常停止:下面接着来看一种情况

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * @author Chen
 * @Description 用volatile不能正常停止的例子
 * @create 2019-11-07 19:07
 */
public class VolatileCannotStop {
    public static void main(String[] args) throws InterruptedException {
        //一种阻塞队列  当元素为空或元素满时都会进入阻塞状态
        ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
        Producer producer = new Producer(storage);
        Thread producerThread = new Thread(producer);
        producerThread.start();
        Thread.sleep(1000);


        Consumer consumer = new Consumer(storage);
        while (consumer.needMoreNums()) {
            System.out.println(consumer.storage.take() + "被消费了");
            Thread.sleep(100);
        }
        System.out.println("消费者不需要更多数据了。");
        //一旦消费不需要更多数据了,我们应该让生产者也停下来
        producer.canceled = true;
        System.out.println(producer.canceled);
    }
}

/**
 * 生产者
 */
class Producer implements Runnable {

    public volatile boolean canceled = false;
    BlockingQueue storage;

    public Producer(BlockingQueue storage) {
        this.storage = storage;
    }

    @Override
    public void run() {
        int num = 0;
        try {
            while (num <= 100000 && !canceled) {
                if (num % 100 == 0) {
                    storage.put(num);
                    System.out.println(num + "是10的倍数,被放入到仓库了。");
                }
                num++;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("生产者结束运行");
        }
    }
}

/**
 * 消费者
 */
class Consumer {
    BlockingQueue storage;

    public Consumer(BlockingQueue storage) {
        this.storage = storage;
    }

    public boolean needMoreNums() {
        if (Math.random() > 0.95) {
            return false;
        }
        return true;
    }
}

imagef6ff7f7ffdb473d6.png

这是一个生产者消费者模式的例子,生产者负责不断生产消息放入到storage中,而消费者随机的进行消费数据。一旦不需要生产者生产数据了就把标志位设置为true。这种方法看似是可行的,然而运行却遇到了图中的情况,程序并没有停止下来。

这是因为生产者生产的速度很快,消费者消费了几个消息后设置标记位为true的时候此时生产者的队列中已经满了,所以会阻塞在storage.put(num)这里,从而无法检测到标记位的变化。

此时如果使用interrupt方法则不会出现这种情况。

停止线程相关的方法解析

判断是否已被中断的方法:

static boolean interrupted():获取中断标志并重置,目标对象为当前执行它的线程,与谁调用这个方法无关

boolean isInterrupted():获取中断标志

例子:

/**
 * @author Chen
 * @Description
 * @create 2019-11-07 20:10
 */
public class RightWayInterrupted {

    public static void main(String[] args) throws InterruptedException {

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                for (; ; ) {
                }
            }
        });

        // 启动线程
        threadOne.start();
        //设置中断标志
        threadOne.interrupt();
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        //获取中断标志并重置
        System.out.println("isInterrupted: " + threadOne.interrupted());
        //获取中断标志并重直
        System.out.println("isInterrupted: " + Thread.interrupted());
        //获取中断标志
        System.out.println("isInterrupted: " + threadOne.isInterrupted());
        threadOne.join();
        System.out.println("Main thread is over.");
    }
}

运行结果:

isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true

注意:threadOne.interrupted()Thread.interrupted()作用其实是一样的

总结

如何停止线程?

用interrupt来处理,而不是stop或者volatile等方法。把主动权交给被中断的线程。这样可以保证线程的安全停止。

要想达到这样的效果不仅仅需要调用interrupt方法,而且需要被请求方,被停止方和子方法被调用方相互配合。作为请求方,就是调用Thread.interrupt()发送一个中断请求的信号,而被停止方必须在适当的时候去检查这个中断信号,并且在可能抛出InterruptedException的时候去处理这个信号。

如果我们是去写子方法的,这个子方法需要被线程所调用的,应该注意这时应该优先在字方法抛出Exception,以便其他人去处理,也可以进行try/catch后重新设置中断标志位。


原文链接:https://www.cnblogs.com/chen88/p/11815336.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:Java 8 - 行为参数化

下一篇:Netty与RPC