第五章、核心3:线程停止、中断之最佳实践

1、原理介绍:使用interrupt来通知,而不是强制

  • interrupt是中断,A线程通知B线程去中断,而B线程是具有主动权的。B线程何时停止是B线程自己决定的,可能根据当前业务逻辑完成情况,所以说是通知,而不是强制

2、停止线程的最佳实践

2.1、通常线程会在什么情况下停止

  • run方法的所有代码都执行完毕了
  • 有异常出现且没有捕获

2.2、正确的停止方法:interrupt

2.2.1、普通情况下停止线程

  • thread.interrupt通知,同时在thread的run方法中对interrupt状态进行响应
/**
 * RightWayStopThreadWithoutSleep
 *
 * @author venlenter
 * @Description: run方法内没有sleep和wait方法时,停止线程
 * @since unknown, 2020-04-05
 */
public class RightWayStopThreadWithoutSleep implements Runnable {
    @Override
    public void run() {
        int num = 0;
        //run方法里面需要做相关的相应判断逻辑,如果不加!Thread.currentThread().isInterrupted(),等效是没有接受到interrupt通知,也就不会做停止的逻辑
        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 RightWayStopThreadWithoutSleep());
        thread.start();
        Thread.sleep(1000);
        //使用interrupt去通知中断
        thread.interrupt();
    }
}

2.2.2、线程被阻塞的情况(sleep)

/**
 * RightWayStopThreadWithSleep
 *
 * @author venlenter
 * @Description:
 * @since unknown, 2020-04-05
 */
public class RightWayStopThreadWithSleep {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                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();
    }
}
//输出结果
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadWithSleep.lambda$main$0(RightWayStopThreadWithSleep.java:21)
	at java.lang.Thread.run(Thread.java:748)

2.2.3、如果线程在每次迭代(for/while)后都阻塞

/**
 * RightWayStopThreadWithSleepEveryLoop
 *
 * @author venlenter
 * @Description: 如果在执行过程中,每次循环都会调用sleep和wait等方法,那么不需要每次迭代都检查是否已中断
 * @since unknown, 2020-04-05
 */
public class RightWayStopThreadWithSleepEveryLoop {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            int num = 0;
            try {
                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();
    }
}
//输出结果
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
400是100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadWithSleepEveryLoop.lambda$main$0(RightWayStopThreadWithSleepEveryLoop.java:20)
	at java.lang.Thread.run(Thread.java:748)

2.2.4、while内try/catch的问题

/**
 * CanInterrupt
 *
 * @author venlenter
 * @Description: 在while中使用try catch
 * @since unknown, 2020-04-05
 */
public class CanInterrupt {
    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();
    }
}
//输出结果
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
400是100的倍数
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.CanInterrupt.lambda$main$0(CanInterrupt.java:20)
	at java.lang.Thread.run(Thread.java:748)
500是100的倍数
600是100的倍数
...
  • 为什么加了Thread.currentThread().isInterrupted()判断后,线程没有停止,仍然继续执行?:其实是因为sleep响应中断后,会把interrupt状态清除,所以中断信号无效

2.2.5、实际开发中的2种最佳实践

  • 优先选择:传递中断(将中断向上抛出,由顶层run方法来处理中断)
/**
 * RightWayStopThreadInProd
 *
 * @author venlenter
 * @Description: 最佳实践:catch了InterruptException之后的优先选择:在方法签名中抛出异常
 * 那么在run()就会强制try/catch
 * @since unknown, 2020-04-05
 */
public class RightWayStopThreadInProd 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 {
        //在方法签名中抛出异常
        Thread.sleep(2000);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new RightWayStopThreadInProd());
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}
//输出结果
go
保存日志逻辑
go //中断后,interrupt标志位被清除,所以会不断循环打印go
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd.throwInMethod(RightWayStopThreadInProd.java:26)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd.run(RightWayStopThreadInProd.java:17)
	at java.lang.Thread.run(Thread.java:748)
go
  • 不想或无法传递:恢复中断
/**
 * RightWayStopThreadInProd2
 *
 * @author venlenter
 * @Description: 最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,
 * 以便于在后续的执行中,依然能够检查到刚才发生了中断
 * 回到刚才RightWayStopThreadInProd补上中断,让它跳出
 * @since unknown, 2020-04-05
 */
public class RightWayStopThreadInProd2 implements Runnable {
    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("Interrupted,程序运行结束");
                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();
    }
}
//输出结果
java.lang.InterruptedException: sleep interrupted
Interrupted,程序运行结束
	at java.lang.Thread.sleep(Native Method)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd2.reInterrupt(RightWayStopThreadInProd2.java:26)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.RightWayStopThreadInProd2.run(RightWayStopThreadInProd2.java:20)
	at java.lang.Thread.run(Thread.java:748)
  • 不应屏蔽中断

2.2.6、响应中断的方法总结列表

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

3、停止线程的错误方法

3.1、被弃用的stop、suspend和resume方法

  • 用stop()来停止线程,会导致线程运行一半突然停止
/**
 * StopThread
 *
 * @author venlenter
 * @Description: 错误的停止方法:用stop()来停止线程,会导致线程运行一半突然停止,
 * 没办法完成一个基本单位的操作(一个连队),会造成脏数据(有的连队多领取少领取装备)
 * @since unknown, 2020-04-05
 */
public class StopThread implements Runnable {
    @Override
    public void run() {
        //模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
        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
  • suspend将线程挂起,运行->阻塞;调用后并不释放所占用的锁(不释放锁,可能会导致死锁)
  • resume将线程解挂,阻塞->就绪(不释放锁,可能会导致死锁)

3.2、用volatile设置boolean标记位

3.2.1 看上去可行

/**
 * WrongWayVolatile
 *
 * @author venlenter
 * @Description: 演示用volatile的局限:part1看似可行
 * @since unknown, 2020-04-05
 */
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 r = new WrongWayVolatile();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(5000);
        r.canceled = true;

    }
}
//输出结果
0是100的倍数
100是100的倍数
200是100的倍数
...

3.2.2 错误之处

  • 代码演示
/**
 * WrongWayVolatileCantStop
 *
 * @author venlenter
 * @Description: 演示用volatile的局限part2
 * 陷入阻塞时,volatile是无法停止线程的
 * 此例中,生产者的生产速度很快,消费者消费速度慢,所以阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
 * @since unknown, 2020-04-05
 */
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;
    }
}

/**
 * 生产者
 */
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 < 10000 && !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() {
        return Math.random() > 0.95 ? false : true;
    }
}

//输出结果
0是100的倍数,被放到仓库中了。
100是100的倍数,被放到仓库中了。
200是100的倍数,被放到仓库中了。
300是100的倍数,被放到仓库中了。
400是100的倍数,被放到仓库中了。
500是100的倍数,被放到仓库中了。
600是100的倍数,被放到仓库中了。
700是100的倍数,被放到仓库中了。
800是100的倍数,被放到仓库中了。
900是100的倍数,被放到仓库中了。
1000是100的倍数,被放到仓库中了。
0被消费了
100被消费了
1100是100的倍数,被放到仓库中了。
200被消费了
1200是100的倍数,被放到仓库中了。
1300是100的倍数,被放到仓库中了。
300被消费了
1400是100的倍数,被放到仓库中了。
400被消费了
1500是100的倍数,被放到仓库中了。
500被消费了
600被消费了
1600是100的倍数,被放到仓库中了。
700被消费了
1700是100的倍数,被放到仓库中了。
1800是100的倍数,被放到仓库中了。
800被消费了
900被消费了
1900是100的倍数,被放到仓库中了。
1000被消费了
2000是100的倍数,被放到仓库中了。
消费者不需要更多数据了。
//线程没有停止
  • 错误原因:线程阻塞在storage.put(num),无法继续执行
  • 修正方式

3.2.3 修正方案

/**
 * WrongWayVolatileFixed
 *
 * @author venlenter
 * @Description: 使用interrupt方式正确处理生产者、消费者
 * @since unknown, 2020-04-05
 */
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("消费者不需要更多数据了。");
        producerThread.interrupt();
    }

    /**
     * 生产者
     */
    class Producer implements Runnable {
        BlockingQueue storage;

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

        @Override
        public void run() {
            int num = 0;
            try {
                while (num < 10000 && !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 {
        BlockingQueue storage;

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

        public boolean needMoreNums() {
            return Math.random() > 0.95 ? false : true;
        }
    }
}
//输出结果
...
3800是100的倍数,被放到仓库中了。
2900被消费了
3900是100的倍数,被放到仓库中了。
4000是100的倍数,被放到仓库中了。
3000被消费了
java.lang.InterruptedException
消费者不需要更多数据了。
生产者结束运行
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
	at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
	at ConcurrenceFolder.mooc.threadConcurrencyCore.stopthreads.volatiledemo.WrongWayVolatileFixed$Producer.run(WrongWayVolatileFixed.java:51)
	at java.lang.Thread.run(Thread.java:748)

4、重要函数的源码解析

4.1 interrupt方法

4.1.1 判断是否已被中断相关方法

  • static boolean interrupted():判断是否中断,同时清除中断状态
 public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    
private native boolean isInterrupted(boolean ClearInterrupted);
  • boolean isInterrupted()
  • Thread.interrupted()的目的对象:静态interrupted只跟当前线程有关,与对象无关

5、停止线程——常见面试问题

5.1 如何停止线程

  • 1、原理:用interrupt来请求,好处是可以保证数据安全,应该把主动权交给被中断的线程
  • 2、想停止线程,要请求方、被停止方(在线程run中检测interrupt状态)、子方法被调用方(内部调用方法将中断向上抛出,由顶层run来做处理,而不是吞掉中断异常)相互配合
  • 3、最后再说错误的方法:stop/suspend已废弃(不释放锁,可能导致死锁),volatile的boolean无法处理长时间阻塞的情况

5.2 如何处理不可中断的阻塞

  • 针对特定的场景,用特定的方法来处理

笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Venlenter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值