Java 中如何正确停止线程

背景

这里将着重描述如何停止一个线程,同时,顺带对其他线程方法如join、yield、wait方法也做一个总结。

1. stop方法


这个方法其实很暴力。有点像站在命运掌控者的视角,让一个国家皇帝立刻马上去死。后事还没有安排,皇帝老爷就忽然驾崩了,导致后续皇权可能会平稳过度,也可能让国家陷入战乱。
我们来看一个生产消费者模型的实例:

        public static void main(String args[]) {
            Queue<Integer> buffer = new LinkedList<>();
            int maxSize = 10;
            Thread producer = new Producer(buffer, maxSize, "PRODUCER");
            Thread consumer = new Consumer(buffer, maxSize, "CONSUMER");
            new Thread(()->{
                try {
                    Thread.sleep(300);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                producer.stop();
            }).start();
            producer.start();
            consumer.start();
        }
    }

    class Producer extends Thread {
        private Queue<Integer> queue;
        private int maxSize;
        int counter =0 ;

        public Producer(Queue<Integer> queue, int maxSize, String name) {
            super(name);
            this.queue = queue;
            this.maxSize = maxSize;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == maxSize) {
                        try {
                            System.out.println("Queue is full.");
                            queue.wait();
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }
                    try {
                        System.out.println("Producing value : " + ++counter);
                        //通过睡眠,模拟生产过程
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    queue.add(counter);
                    queue.notifyAll();
                }
            }
        }
    }

     
    class Consumer extends Thread {
        private Queue<Integer> queue;
        private int maxSize;

        public Consumer(Queue<Integer> queue, int maxSize, String name) {
            super(name);
            this.queue = queue;
            this.maxSize = maxSize;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        System.out.println("Queue is empty.");
                        try {
                            queue.wait();
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }
                    System.out.println("Consuming value : " + queue.remove());
                    queue.notifyAll();
                }
            }
        }

说明:

  1. 生产过程需要假设需要500毫秒左右,然而我们在300毫秒左右的时候,关掉了生产者线程,这个时候,生产过程已经开始但是并没有完成;
  2. 当关闭生产者线程时,它会释放自己所持有的所有锁,此时消费者线程获取了queue锁,当进入消费过程,却发现队列是空的;
  3. 假设这样的场景:生产者线程是意外关掉的,而生产过程执行的是一件很重要的任务,数据很敏感,这时候通过stop这种暴力手段将可能带来一场灾难,所产生的后果是未知的。
    JDK文档中说:

这个方法天生就是不安全的。通过这种方式停止一个线程,将导致该线程解锁所有的监视器对象(即上面中的queue)。如果某一对象(queue)的一致性受到该监视器的?;?,暴力停止线程所造成的对对象的破坏对其他线程是可见的。

其实,不仅仅是锁的问题,又比如读写文件的过程,在文件写入过程中,随便stop的线程,将导致写入内容无法被预测,即我们无法知道它写入了多少。

1.1 使用条件变量停止一个线程

一种比较安全的停止线程的方式是:告诉想要停止的线程,我想要关闭你,有些后事赶紧处理下。
比如,我们可以添加一个条件变量flag:

class TerminateClassByFlag extends Thread {
    private volatile boolean terminateFlag;
    private int counter;

    public void terminate() {
        terminateFlag = true;
    }

    @Override
    public void run() {
        while (!terminateFlag) {
            try {
                Thread.sleep(1000);
                System.out.println("counter:" + counter++);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在需要停止该线程的时候,调用terminate方法即可。但是,该方法存在一个问题,即在收到terminate指令之后,并不会立刻停止,比如它正在休眠当中。必须是在检查循环条件时,才会因不满足循环条件而退出。

1.2 使用线程的intercept方法停止线程

如果希望能够立刻退出线程,可以使用intercept方法。

public static void main(String[] args) {
        Thread a = new Thread(()->{
            int counter =0;
             while(!Thread.currentThread().isInterrupted()){
                 try {
                    Thread.sleep(1000);
                    System.out.println("cuonter:"+counter++);
                } catch (Exception e) {
                    e.printStackTrace();
                                        //注意这里,需要再次调用intercept
                    Thread.currentThread().interrupt();
                }
                 
             }
        });
        a.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a.interrupt();
    }

intercept方法会中断该线程,中断过程又分了4种情况,JDK文档中说:

  1. If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Object class, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int), methods of this class, then its interrupt status will be cleared and it will receive an InterruptedException.
    如果调用了Object对象的wait,wait(long),wait(long,int)等方法阻塞了线程,或者调用join(),join(long),join(long,int),sleep(long),sleep(long,int)等线程方法,中断标志将被清除,同时会收到一个中断异常。(注意,这就是我在上面的代码中为什么要在catch中重新intercept)。
  2. If this thread is blocked in an I/O operation upon an InterruptibleChannel then the channel will be closed, the thread's interrupt status will be set, and the thread will receive a ClosedByInterruptException.
    如果该线程正阻塞于interruptible channel上的I/O操作,则该通道将被关闭,同时该线程的中断状态被设置,并收到一个java.nio.channels.ClosedByInterruptException。
  3. If this thread is blocked in a Selector then the thread's interrupt status will be set and it will return immediately from the selection operation, possibly with a non-zero value, just as if the selector's wakeup method were invoked.
    如果该线程正阻塞于一个java.nio.channels.Selector操作,则该线程的中断状态被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用java.nio.channels.Selector.wakeup()方法一样。
  4. If none of the previous conditions hold then this thread's interrupt status will be set.
    如果上述条件都不成立,则该线程的中断状态将被设置。

那么,该如何获取中断状态呢?就是通过isInterrupted()方法或者interrupted()方法。
其中前者属于对象的方法,后者是静态方法。并且后者在读取中断状态后,会清除中断状态。

2.join方法


join(), 对于线程A和线程B两个线程,如果在线程B中调用线程A的join方法,则B由wait方法进入阻塞状态,直到A执行完之后notifyAll重新唤醒B。
join实现源码为:

    public final synchronized void join(long millis)    throws InterruptedException {  
        long base = System.currentTimeMillis();  
        long now = 0;  
  
        if (millis < 0) {  
            throw new IllegalArgumentException("timeout value is negative");  
        }  
          
        if (millis == 0) {  
            while (isAlive()) {  
                wait(0);  
            }  
        } else {  
            while (isAlive()) {  
                long delay = millis - now;  
                if (delay <= 0) {  
                    break;  
                }  
                wait(delay);  
                now = System.currentTimeMillis() - base;  
            }  
        }  
    }  

3. yield


JDK文档中指出:

该方法实际上是一个暗示,表明它希望放弃当前的处理器。这种暗示,可能被调度器忽略。

即它会重新进入到可执行状态,但是由于线程优先级缘故,调度器可能选择该线程继续执行。事实上,我没有使用过该方法。JDK文档中也说该方法非常少用 ,主要是帮助进行极少情况的bug重现。

4. wait


进入到监视器对象的阻塞队列中,直到调用了该对象的notify或者notifyAll方法,被唤醒从而拥有了获取监视器主权的条件。
其实,就是排队过独木桥,当一个对象过完独木桥,会调用notify方法,从队列中选取另外一个对象过桥,而notifyAll会唤醒所有等待过桥的对象,让它们去抢这个多桥的机会。

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

推荐阅读更多精彩内容