• 线程中断的方式及vilotile不合适的场景


    原理

    通常情况下,我们不会手动停止一个线程,而是允许线程运行到结束,然后让它自然停止。但是依然会有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等。

    在这种情况下,即将停止的线程在很多业务场景下仍然很有价值。尤其是我们想写一个健壮性很好,能够安全应对各种场景的程序时,正确停止线程就显得格外重要。但是Java 并没有提供简单易用,能够直接安全停止线程的能力。

    为什么不强制停止?而是通知、协作

    最正确的停止线程的方式是使用 interrupt。
    interrupt的作用:通知被停止线程的作用
    为什么只是起到了通知作用:
    而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停

    为什么 Java 不提供强制停止线程的能力呢

    • 希望程序能相互通知、协作的管理线程

    因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。

    如何用interrupt停止线程

    while (!Thread.currentThread().isInterrupted() && more work to do) {
        do more work
    }
    

    如何用代码实现停止线程的逻辑。

    我们一旦调用某个线程的 interrupt() 之后,这个线程的中断标记位就会被设置成 true。每个线程都有这样的标记位,当线程执行时,应该定期检查这个标记位,如果标记位被设置成 true,就说明有程序想终止该线程。回到源码,可以看到在 while 循环体判断语句中,首先通过 Thread.currentThread().isInterrupt() 判断线程是否被中断,随后检查是否还有工作要做。&& 逻辑表示只有当两个判断条件同时满足的情况下,才会去执行下面的工作。

    public class StopThread implements Runnable {
        @Override
        public void run() {
            int count = 0;
            while (!Thread.currentThread().isInterrupted() && count < 1000) {
                System.out.println("count=" + count++);
            }
        }
    
        public static void main(String[] args) throws Exception {
            Thread thread = new Thread(new StopThread());
            thread.start();
            Thread.sleep(30);
            thread.interrupt();
        }
    }
    



    在 StopThread 类的 run() 方法中,首先判断线程是否被中断,然后判断 count 值是否小于 1000。这个线程的工作内容很简单,就是打印 0~999 的数字,每打印一个数字 count 值加 1,可以看到,线程会在每次循环开始之前,检查是否被中断了。接下来在 main 函数中会启动该线程,然后休眠 5 毫秒后立刻中断线程,该线程会检测到中断信号,于是在还没打印完1000个数的时候就会停下来,这种就属于通过 interrupt 正确停止线程的情况。

    sleep 期间能否感受到中断

    public class StopRunningThread {
        public static void main(String[] args) throws Exception {
            Thread thread = new Thread(() -> {
                int num = 1;
                try {
                    while (!Thread.currentThread().isInterrupted() && num <= 1000) {
                        System.out.println(num++);
                        Thread.sleep(100000);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            thread.start();
            Thread.sleep(100);
            thread.interrupt();
        }
    }
    


    结果说明 如果 sleep、wait 等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个 InterruptedException 异常,同时清除中断信号,将中断标记位设置成 false。这样一来就不用担心长时间休眠中线程感受不到中断了,因为即便线程还在休眠,仍然能够响应中断通知,并抛出异常。

    线程中断的两种处理方式

    • 方法抛出InterruptedException
    • try/catch catch再次中断
    private void reInterrupt() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
    

    为什么用 volatile 标记位的停止方法是错误的

    错误的停止方法

    1. stop():直接停止线程,线程没有时间在停止前保存自己的逻辑,会出现数据完整性问题
    2. susppend()和resume()组合:不会释放锁就开始进入休眠,持有锁的情况下很容易出现死锁问题,线程在resume()之前不会释放
    3. volatile标记位
    • 正确停止方法
    public class VolatileCanStop {
        private static boolean cancel = false;
        public static void main(String[] args) throws Exception {
            Thread thread = new Thread(() -> {
                int num = 0;
                while (!cancel && num < 10000) {
                    if (num % 10 == 0) {
                        System.out.println("10的倍数:" + num);
                    }
                    num++;
                    try {
                        Thread.sleep(1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
            Thread.sleep(100);
            cancel = true;
        }
    }
    


    上面代码中使用了Runnable匿名线程,然后在 run() 中进行 while 循环,在循环体中又进行了两层判断,首先判断 canceled 变量的值,canceled 变量是一个被 volatile 修饰的初始值为 false 的布尔值,当该值变为 true 时,while 跳出循环,while 的第二个判断条件是 num 值小于1000000(一百万),在while 循环体里,只要是 10 的倍数就打印出来,然后 num++。

    • 错误停止方法
    1. 首先,声明了一个生产者 Producer,通过 volatile 标记的初始值为 false 的布尔值 canceled 来停止线程。而在 run() 方法中,while 的判断语句是 num 是否小于 100000 及 canceled 是否被标记。while 循环体中判断 num 如果是 50 的倍数就放到 storage 仓库中,storage 是生产者与消费者之间进行通信的存储器,当 num 大于 100000 或被通知停止时,会跳出 while 循环并执行 finally 语句块,告诉大家“生产者结束运行”。
    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 % 50 == 0) {
                        storage.put(num);
                        System.out.println(num + "是50的倍数,被放到仓库中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("生产者结束运行");
            }
        }
    }
    
    1. 而对于消费者 Consumer,它与生产者共用同一个仓库 storage,并且在方法内通过 needMoreNums() 方法判断是否需要继续使用更多的数字,刚才生产者生产了一些 50 的倍数供消费者使用,消费者是否继续使用数字的判断条件是产生一个随机数并与 0.97 进行比较,大于 0.97 就不再继续使用数字。
    class Consumer {
        BlockingQueue storage;
    
        public Consumer(BlockingQueue storage) {
            this.storage = storage;
        }
    
        public boolean needMoreNums() {
            if (Math.random() > 0.97) {
                return false;
            }
            return true;
        }
    }
    
    
    1. main方法,下面来看下 main 函数,首先创建了生产者/消费者共用的仓库 BlockingQueue storage,仓库容量是 8,并且建立生产者并将生产者放入线程后启动线程,启动后进行 500 毫秒的休眠,休眠时间保障生产者有足够的时间把仓库塞满,而仓库达到容量后就不会再继续往里塞,这时生产者会阻塞,500 毫秒后消费者也被创建出来,并判断是否需要使用更多的数字,然后每次消费后休眠 100 毫秒,这样的业务逻辑是有可能出现在实际生产中的。

    当消费者不再需要数据,就会将 canceled 的标记位设置为 true,理论上此时生产者会跳出 while 循环,并打印输出“生产者运行结束”。

    public class VolatileCannotStop {
    
        public static void main(String[] args) throws InterruptedException {
            ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
    
            Producer producer = new Producer(storage);
            Thread producerThread = new Thread(producer);
            producerThread.start();
            Thread.sleep(500);
    
            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);
        }
    }
    

    然而结果却不是我们想象的那样,尽管已经把 canceled 设置成 true,但生产者仍然没有停止,这是因为在这种情况下,生产者在执行 storage.put(num)时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断canceled的值的,所以在这种情况下用 volatile 是没有办法让生产者停下来的,相反如果用 interrupt 语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。

    总结

    1. 中断线程使用interrupt()
    2. 方法声明InterruptException或者catch后Thread.currentThread().interrupted();
    3. 不要使用stop(),suspend() resume()组合被标记过时的方法
    4. 尽量避免使用volatile停止线程
  • 相关阅读:
    bzoj 3040: 最短路(road)
    bzoj 2049: [Sdoi2008]Cave 洞穴勘测
    poj 2505 A multiplication game
    hdu 1729 Stone Game
    经典博弈模型
    hdu 1848 Fibonacci again and again(SG函数)
    hdu 2147 kiki's game(巴什博弈)
    hdu 1847 Good Luck in CET-4 Everybody!(巴什博弈)
    hdu 4388 Stone Game II
    poj 2234 Matches Game
  • 原文地址:https://www.cnblogs.com/javashare/p/15970995.html
Copyright © 2020-2023  润新知