美文网首页
Java线程系列——线程的停止

Java线程系列——线程的停止

作者: 禺沫 | 来源:发表于2020-02-22 23:21 被阅读0次

    一、如何正确的停止线程

    线程的停止有两种情况,一种是程序运行完毕,线程自然停止;而另一种是用户主动取消。
    Java语言的设计,本身并没有一种机制来安全停止线程,停止线程处于一种合作机制。所以,想要停止其他线程其实只是一种通知,而非强制。Java让被中断的线程本身拥有决定权,它不但可以决定何时去响应这个中断,何时去停止,还可以决定停不停止。如果我们想要停止某个线程,但那个线程自己不想被中断,我们对此是无能为力的,这是一种规范。
    其实大多数的时候,想要线程停止,至少要让他运行到结束。比如电脑关机,要让它处理完现在做的事情,保存一下当前的状态等。我们对其他线程一无所知,而且不希望对方陷入混乱,所以线程是否要停止,何时应该停止,应该由自己决定。
    所以,Java中用了interrupt,中断,来通知其他线程,希望对方可以停止。

    那么一下,分情况给出了interrupt方法的使用。

    1. 在没有阻塞的情况下:
    public class RightWayStopThreadWithoutSleep implements Runnable{
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
            thread.start();
            Thread.sleep(1000);
            thread.interrupt();
        }
    
        @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("任务运行结束了");
        }
    }
    

    没有中断的时候,输出 1073740000是10000的倍数,任务运行结束了
    执行thread.interrupt(),通知线程停止,并且被中断的线程也理会此次通知,在循环时判断加入Thread.currentThread().isInterrupted()判断,进行响应,所以输出382800000是10000的倍数,任务运行结束了。可以看出,线程直接停止,并没有抛出异常

    2.在阻塞情况下
    public class RightWayStopThreadWithSleep {
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = () -> {
                int num = 0;
                while (num <= 300 && !Thread.currentThread().isInterrupted()) {
                    if (num % 100 == 0) {
                        System.out.println(num + "是100的倍数");
                    }
                    num++;
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
            Thread thread = new Thread(runnable);
            thread.start();
            Thread.sleep(500);
            thread.interrupt();
        }
    }
    

    运行结果:

    0是100的倍数
    100是100的倍数
    200是100的倍数
    300是100的倍数
    java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at stopthreads.RightWayStopThreadWithSleep.lambdamain0(RightWayStopThreadWithSleep.java:19)
    at java.lang.Thread.run(Thread.java:745)

    可见,当运行thread.interrupt()的时候,线程正好在阻塞状态下,会抛出InterruptedException异常。
    那么当每次循环时,都处于阻塞情况下,如此中断是否可行呢?把上面的代码稍微改一下:

    public class RightWayStopThreadWithSleepEveryLoop {
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = () -> {
                int num = 0;
                while (num <= 1000 && !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(500);
            thread.interrupt();
        }
    }
    

    打印结果:

    0是100的倍数
    java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at stopthreads.RightWayStopThreadWithSleepEveryLoop.lambdamain0(RightWayStopThreadWithSleepEveryLoop.java:17)
    at java.lang.Thread.run(Thread.java:745)
    100是100的倍数
    200是100的倍数
    300是100的倍数

    还在一直执行下去,可见把try/catch放在while中,只会中断循环中的一次执行,并不会退出循环,更不会停止线程,难道循环条件!Thread.currentThread().isInterrupted()失效了么?
    其实,在Java语言的设计中,在响应中断的时候,会再此把响应状态复原,所以想要用Thread.currentThread().isInterrupted()来判断下一次的中断状态是不可行的。

    那么我们把try/catch放在循环外面捕获异常情况下,是否可以如预期,终止线程呢?试一下。

    public class RightWayStopThreadWithSleepEveryLoop {
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = () -> {
                int num = 0;
                try {
                    while (num <= 1000 && !Thread.currentThread().isInterrupted()) {
                        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(500);
            thread.interrupt();
        }
    }
    

    执行结果如下:

    0是100的倍数
    java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at stopthreads.RightWayStopThreadWithSleepEveryLoop.lambdamain0(RightWayStopThreadWithSleepEveryLoop.java:18)
    at java.lang.Thread.run(Thread.java:745)

    可见线程被中断了,程序没有再运行下去。那么进一步思索,在循环中加入!Thread.currentThread().isInterrupted()判断,是否画蛇添足了呢?Thread.sleep()方法本身可以抛出异常,而且循环条件判断运行时间过短,其实没有必要再在此进行中断状态的判断了。

    3.从代码架构角度来讲,很多时候,我们对中断异常的捕获,很可能是捕获一个抛出异常的方法,那么如何来处理呢?
    public class RightWayStopThreadInProd implements Runnable {
        @Override
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("go");
                    throwInMethod();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private void throwInMethod() throws InterruptedException {
            Thread.sleep(600);
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(new RightWayStopThreadInProd());
            thread.start();
            Thread.sleep(1000);
            thread.interrupt();
        }
    }
    

    运行结果如下:

    go
    go
    java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at
    stopthreads.RightWayStopThreadInProd.throwInMethod(RightWayStopThreadInProd.java:28)
    at stopthreads.RightWayStopThreadInProd.run(RightWayStopThreadInProd.java:19)
    at java.lang.Thread.run(Thread.java:745)

    线程可以正常停止,是没有问题的。那么是否有其他的写法?可以把子方法中的中断异常传递到主方法中呢?

    public class RightWayStopThreadInProd2 implements Runnable {
        @Override
        public void run() {
            while (true) {
                if(Thread.currentThread().isInterrupted()){
                    System.out.println("程序运行结束");
                    break;
                }
                reInterrupt();
            }
        }
    
        private void reInterrupt()  {
            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();
        }
    }
    

    可见,这种传递是可行的。而且在主方法中处理异常是更恰当的处理方式。

    二、响应中断的方法列表

    Object.wait()/wait(long)/wait(long,int)
    Thread.sleep(long)/sleep(long int)
    Thread.join()/join(long)/join(long,int)
    Java.util.concurrent.BlockingQueue.take()/put(E)
    Java.util.concurrent.locks.Lock.lockInterruptibly()
    Java.util.concurrent.CountDownLatch.await()
    Java.util.concurrent.CyclicBarrier.await()
    Java.util.concurrent.Exchanger.exchange(V)
    Java.nio.channels.InterruptibleChannel相关方法
    Java.nio.channels.Selector的相关方法

    三、错误停止的方法

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

    用stop来停止线程,会导致线程运行一半突然停止,没有办法完成一个基本单位的操作。

    public class StopThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                System.out.println("连队" + i + "开始领取武器");
                for (int j = 0; j < 10; j++) {
                    System.out.println(j);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("连队" + i + "已经领取完毕");
            }
        }
    
        public static void main(String[] args) {
            Thread thread = new Thread(new StopThread());
            thread.start();
            try {
                thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            thread.stop();
        }
    }
    

    运行结果:

    连队0开始领取武器
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    连队0已经领取完毕
    连队1开始领取武器
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9

    可见,用stop并不能完整执行完,会导致数据错乱。
    关于为什么弃用Thread.stop,来看一下Oracle官网上的解释:
    因为它本质上是不安全的。停止线程会导致它解锁已锁定的所有监视器。(当ThreadDeath异常传播到堆栈中时,监视器将被解锁。)如果先前受这些监视器保护的任何对象处于不一致状态,则其他线程现在可以以不一致的状态查看这些对象,据说这些物体已被损坏。当线程对受损对象进行操作时,可能会导致任意行为。这种行为可能很微妙并且难以检测,或者可能是明显的。与其他未经检查的异常不同,它会ThreadDeath默默地杀死线程;因此,用户没有警告他的程序可能被损坏。腐败可能在实际损坏发生后的任何时间显现,甚至未来几小时或几天。
    suspend并不会破坏对象,它会让一个线程挂起,而且是带着锁挂起的,这样就很容易造成死锁。如果其他线程不及时把它唤醒,或者需要这把锁才能将它唤醒,则会造成死锁。

    2.用volatile设置boolean标记位

    这种错误的方法,为什么看起来是可行的

    public class WrongWayVolatile implements Runnable {
        private volatile boolean canceled = false;//可见性
        @Override
        public void run() {
            int num = 0;
            try {
                while (num <= 10000 && !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 {
            WrongWayVolatile wrongWayVolatile = new WrongWayVolatile();
            Thread thread = new Thread(wrongWayVolatile);
            thread.start();
            Thread.sleep(500);
            wrongWayVolatile.canceled = true;
        }
    }
    

    运行结果如下:

    0是100的倍数。
    100是100的倍数。
    200是100的倍数。
    300是100的倍数。
    400是100的倍数。

    运行结果也正确,可以停下来。但是这种方法存在局限性,也就是在非阻塞情况下是可行的。那么阻塞的情况会是怎么样的呢?那么我们先来验证一下最常用的生产者消费者模式。

    public class WrongWayVolatileCantStop {
        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;
        private 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 + "是100的倍数,被放到仓库中了。");
                    }
                    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;
        }
    }
    

    运行结果:

    0是100的倍数,被放到仓库中了。
    100是100的倍数,被放到仓库中了。
    200是100的倍数,被放到仓库中了。
    300是100的倍数,被放到仓库中了。
    400是100的倍数,被放到仓库中了。
    500是100的倍数,被放到仓库中了。
    600是100的倍数,被放到仓库中了。
    700是100的倍数,被放到仓库中了。
    800是100的倍数,被放到仓库中了。
    900是100的倍数,被放到仓库中了。
    0被消费了
    1000是100的倍数,被放到仓库中了。
    消费者不需要更多数据了。
    true

    但实际上,线程并没有停止。
    原因,当线程在长时间阻塞情况下是无法即使唤醒的。一直阻塞在storage.put(num),而没有再去判断while中的循环条件。看interrupt是否可以停止这种。

    public class WrongWayVolatileFixed {
    
        public static void main(String[] args) throws InterruptedException {
            WrongWayVolatileFixed body = new WrongWayVolatileFixed();
            ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
    
            Producer producer = body.new Producer(storage);
            Thread producerThread = new Thread(producer);
            producerThread.start();
            Thread.sleep(1000);
    
            Consumer consumer = body.new Consumer(storage);
            while (consumer.needMoreNums()) {
                System.out.println(consumer.storage.take() + "被消费了");
                Thread.sleep(100);
            }
            System.out.println("消费者不需要更多数据了。");
            //一旦消费不需要更多数据了,我们应该让生产者也停下来
    //        producer.canceled = true;
            producerThread.interrupt();
    //        System.out.println(producer.canceled);
        }
    
        class Producer implements Runnable {
            public volatile boolean canceled = false;
            private BlockingQueue storage;
            public Producer(BlockingQueue storage) {
                this.storage = storage;
            }
    
            @Override
            public void run() {
                int num = 0;
                try {
                    while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
                        if (num % 100 == 0) {
                            storage.put(num);
                            System.out.println(num + "是100的倍数,被放到仓库中了。");
                        }
                        num++;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("生产者结束运行");
                }
            }
        }
    
        class Consumer {
            private BlockingQueue storage;
            public Consumer(BlockingQueue storage) {
                this.storage = storage;
            }
    
            public boolean needMoreNums() {
                if (Math.random() > 0.95) {
                    return false;
                }
                return true;
            }
        }
    }
    

    运行结果:

    8000被消费了
    9000是100的倍数,被放到仓库中了。
    8100被消费了
    9100是100的倍数,被放到仓库中了。
    消费者不需要更多数据了。
    生产者结束运行
    java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizerConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014) at java.util.concurrent.locks.AbstractQueuedSynchronizerConditionObject.await(AbstractQueuedSynchronizer.java:2048)
    at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
    at stopthreads.WrongWayVolatileFixed$Producer.run(WrongWayVolatileFixed.java:51)
    at java.lang.Thread.run(Thread.java:745)

    可以正常停止下来

    四、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();
    }
    
    interrupt源码.png

    可以看出,在阻塞状态下,线程也是可以被唤醒执行中断的。

    五、一些相近方法的解析:

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    
    public class RightWayInterrupted {
        public static void main(String[] args) {
            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());
        }
    }
    

    运行结果:

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

    说明interrupted的是执行这个方法的线程,和threadOne或者Thread谁执行没有关系没有关系

    相关文章

      网友评论

          本文标题:Java线程系列——线程的停止

          本文链接:https://www.haomeiwen.com/subject/yxwhqhtx.html