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

一、如何正确的停止线程

线程的停止有两种情况,一种是程序运行完毕,线程自然停止;而另一种是用户主动取消。
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谁执行没有关系没有关系

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容