关于java:如何优雅关闭一个线程

6次阅读

共计 4823 个字符,预计需要花费 13 分钟才能阅读完成。

当咱们应用多线程的时候,往往有一些场景,须要咱们将正在执行的线程给停掉,比如说,当咱们下载文件的时候,下载到一半不想下载了,这时咱们心愿能够勾销下载操作,该怎么操作呢?

为什么不能用 stop

当咱们去 Thread 类外面找相干的接口时,发现有 个 stop 办法,看上去非常适合用来终止一个线程,然而这个办法下面标了个 @Deprecated 注解,非常明显,这是一个 废除办法,不倡议应用它。次要有两个方面的起因:

  1. 因为这个办法会将线程间接杀掉,没有任何喘息机会,一旦线程被杀死,前面的代码逻辑就再也无奈失去执行,而且咱们无奈确定线程敞开的机会,也就是说线程有可能在任何一行代码忽然进行执行,这是十分危险的。
  2. 如果这个线程正持有某个锁,贸然将其杀死,会导致该线程持有的锁马上被开释,而已经被该锁爱护的资源,可能正处于一种非原子的状态中,此时被其余线程拜访到,会产生不可预知的危险。

针对于第二种状况,可能不是很好了解,上面通过一个例子,阐明一下:

public class StopDemo {public static void main(String[] args) {new ReadThread().start();
        ChangeThread changeThread = new ChangeThread();
        changeThread.start();
        Sleep.seconds(2);
        changeThread.stop();}
    
    private static int num;

    private static class ChangeThread extends Thread {public ChangeThread() {super("change-thread");
        }
      
        @Override
        public void run() {while (true) {synchronized (StopDemo.class) {
                    num++;
                    Sleep.seconds(1);
                    num--;
                }
            }
        }
    }

    private static class ReadThread extends Thread{public ReadThread() {super("read-thread");
        }
      
        @Override
        public void run() {while (true){synchronized (StopDemo.class){if(num!=0){Debug.debug("num 值为:{}",num);
                        break;
                    }
                }
            }
        }
    }
}

首先,在 main 办法中,咱们启动了两个线程,其中一个是 change-thread 线程,用于批改变量 num 的值,先加 1 期待 1s 后再减 1,另一个是 read-thread 线程用于读取共享变量 num 的值,如果不为 0 则打印日志并退出。

因为读写线程用了同一把互斥锁,所以对于共享变量 num 的读和写是互斥的,失常状况下写线程肯定是实现 + 1 再休眠 1s 再 - 1 这样的原子操作后,才会让开释锁,因而 读线程读到的值肯定是 0 ,不会打印日志也不会退出。然而,因为咱们在第 8 行代码执行了changeThread.stop(),可能导致写线程将变量加 1 后就间接退出了,最终读线程读到的值是 1 而不是 0,也退出循环。

两阶段终止

既然 stop() 不倡议应用,那是否有其余方法用来优雅的进行一个线程呢?答案是必须的,那就是两阶段终止 (Two-phase Termination) 计划。两阶段终止的两个阶段别离是指:

  1. 筹备阶段:收回终止指令,通过设置中断标记, 并发送中断信号,“告诉”指标线程,能够筹备进行了。
  2. 执行阶段:响应终止指令,接管到中断信号及标记,在此基础决定线程退出机会,并执行适当清理工作。

<img src=”https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210314114324792.png” alt=”image-20210314114324792″ style=”zoom:50%;” />

在筹备阶段,咱们须要做两件事件:

  1. 设置中断标记:中断标记的作用是标识线程曾经中断了,当线程读到这个标识后,就能够执行退出操作了。
  2. 收回中断信号:光设置中断标记还不行,如果线程以后处于阻塞状态,就算设置了中断标记,线程也无奈检测到,这时就须要咱们调用指标线程的中断办法 interrupt(),将其从阻塞状态中唤醒,而指标线程通过捕捉InterruptedException 异样,来侦测这个中断信号。须要留神的是,线程类中跟中断相干的 api 次要有以下三个,特地容易混同,须要留神辨别:

    办法签名 作用
    public void interrupt() 中断线程
    public static boolean interrupted() 判断以后线程是否为中断状态并革除中断状态
    public boolean isInterrupted() 判断线程是否为中断状态

能够看出,两阶段终止计划相比于 stop() 计划要更加优雅,打个比方,如果说 stop 像是不问青红皂白间接将罪犯就地正法,那两阶段终止计划就像是先将罪犯收押,待事件上不着天; 下不着地之后,再执行相应处罚,相对而言,这种形式更加人性化也很好的防止了冤假错案的产生。

实例

理论知识曾经讲了很多,接下来,咱们通过一个简略的例子,来看看到底如何通过两阶段终止线程,来实现文章结尾所说的勾销下载的性能。

public class TwoPhaseTerminationDemo {

    private static int percent;
    private static final int MAX = 100;

    public static void main(String[] args) throws InterruptedException {DownloadThread downloadThread = new DownloadThread();
        downloadThread.start();
        Thread.sleep(4000);
        downloadThread.stopMe();}

    private static class DownloadThread extends Thread{public DownloadThread() {super("download-thread");
        }

        @Override
        public void run() {while (true){if (isTerminated()){Debug.debug("勾销下载, 退出");
                    break;
                }
                try {Thread.sleep(1000);
                    Debug.debug("已下载: {}%",++percent);
                    if(percent >= MAX){Debug.debug("下载实现");
                    }
                } catch (InterruptedException e) {}}
        }

        public void stopMe(){interrupt();
        }

        public boolean isTerminated(){return Thread.currentThread().isInterrupted();}
    }
}

为了不便了解,这个例子对实在的下载做了简化模仿,假如下载线程每秒钟下载总进度的 1%,在下载了 4s 后,执行 stopMe() 调用线程中断办法 interrupt(),收回中断信号,第 22 行,线程检测状态,如果为中断状态,则打印 ” 勾销下载“ 并退出,能够看到,这个程序曾经满足了上述所说的两阶段终止条件,然而当咱们执行代码后,会发现程序仍然刚愎自用的下载着,没有要停下来的意思。

2021-03-14 13:41:53 [download-thread] 已下载: 1%
2021-03-14 13:41:54 [download-thread] 已下载: 2%
2021-03-14 13:41:55 [download-thread] 已下载: 3%
2021-03-14 13:41:57 [download-thread] 已下载: 4%
2021-03-14 13:41:58 [download-thread] 已下载: 5%
2021-03-14 13:41:59 [download-thread] 已下载: 6%
2021-03-14 13:42:00 [download-thread] 已下载: 7%

起因是线程中断异样被捕捉后,它的中断状态曾经被 jvm 给革除了,所以咱们须要从新设置一下线程的中断状态,在第 34 行加上以下代码

Thread.currentThread().interrupt();

这次再执行,会发现下载操作被勾销了,达到了咱们想要的后果

2021-03-14 13:53:13 [download-thread] 已下载: 1%
2021-03-14 13:53:14 [download-thread] 已下载: 2%
2021-03-14 13:53:15 [download-thread] 已下载: 3%
2021-03-14 13:53:16 [download-thread] 勾销下载, 退出

然而不要快乐的太早,因为这种写法并不完满,它依赖于线程的中断状态来退出线程,如果指标线程的代码中调用了第三方类库的接口,而这些接口在捕捉中断异样后,清空了线程中断状态,然而没有重置,就会导致下面形容的那种谬误状况,所以咱们须要寻求更牢靠的解决方案。

自定义中断标记

下面有提到,咱们不能依赖于线程本身的中断状态,那么正确的做法应该怎么解决呢?其实只有自定义一个中断标记就行了。具体做法如下:

public class ImproveTwoPhaseTerminationDemo {

    private static int percent;
    private static final int MAX = 100;
   
    public static void main(String[] args) throws InterruptedException {DownloadThread downloadThread = new DownloadThread();
        downloadThread.start();
        Thread.sleep(4000);
        downloadThread.stopMe();}

    private static class DownloadThread extends Thread{

        private boolean terminated = false;// 自定义中断标记
        
        public DownloadThread() {super("download-thread");
        }

        @Override
        public void run() {while (true){if (isTerminated()){Debug.debug("勾销下载, 退出");
                    break;
                }
                try {Thread.sleep(1000);
                    Debug.debug("已下载: {}%",++percent);
                    if(percent >= MAX){Debug.debug("下载实现");
                    }
                } catch (InterruptedException e) {Thread.currentThread().interrupt();}
            }
        }

        public void stopMe(){
            terminated = true;
            interrupt();}

        public boolean isTerminated(){return terminated;}
    }
}

能够看到,第 15 行加上了一个终止标记 terminated,调用stopMe() 办法的时候,将 terminated 设置为 true,通过这个标记,咱们就能够不依赖于线程本身的中断状态,而将线程进行中断了。

总结

这篇文章次要解说了如何优雅的敞开一个线程,首先咱们应该防止应用 stop() 办法,这种办法简略粗犷但具备不确定性,容易造成 bug,正确的做法是通过两阶段终止计划,先收回中断请求,设置线程为中断状态,当线程侦测到中断状态后,再去执行中断后的清理逻辑。

公众号:Java 多线程实战,专一于分享 java 多线程编程干货,关注收费支付海量学习材料

正文完
 0