如何正确停止Java线程,终止Java线程的三种方法

御弟哥哥 等级 378 0 0

如何正确停止Java线程,终止Java线程的三种方法

在 Java 中有以下 3 种方法可以终止正在运行的线程:

  1. 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程终止。
  2. 使用 stop() 方法强行终止线程,但不推荐,该方法已被弃用,原因见后文。
  3. 使用 interrupt 方法中断线程。

以下内容翻译自 JDK1.5官方文档 ,内容有微调。未发现JDK1.8对应文档与JDK1.5的内容有明显不同。

停止一个线程的推荐做法

stop的大多数用法应由简单地修改某些变量以指示目标线程应停止运行的代码代替。 目标线程应定期检查此变量,如果该变量指示要停止运行,则应有序地从其运行方法返回。 (这是Java始终推荐的方法)。为了确保对stop-request进行及时的通信,变量必须是volatile(或必须同步访问变量)

例如,假设您的程序包含以下startstoprun方法:

 private Thread blinker;

    public void start() {
        blinker = new Thread(this);
        blinker.start();
    }

    public void stop() {
        blinker.stop();  // UNSAFE!
    }

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (true) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e){
            }
            repaint();
        }
    } 

可以通过将程序的stop和run方法替换为下面的代码来避免使用Thread.stop

 private volatile Thread blinker;

    public void stop() {
        blinker = null;
    }

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e){
            }
            repaint();
        }
    } 


=== 后面的内容可以不看,看的话务必从头到尾按顺序阅读 ===



为什么 Thread.stop 被废弃?

因为它本质上是不安全的。 _停止线程会使它解锁它已锁定的所有监视器_。 (当ThreadDeath异常在堆栈中向上传播时,监视器将被解锁。)如果先前由这些监视器保护的任何对象处于不一致状态,则其他线程现在可能会以不一致状态查看这些对象。 这样的对象被称为_已损坏的对象_。 当线程对损坏的对象进行操作时,可能会导致任意行为。 此行为可能是微妙的,难以检测,或者可能是明显的。 与其他未检查的异常不同,ThreadDeath会无声地杀死线程。 因此,用户没有警告其程序可能已损坏。 在实际损坏发生后的任何时间,腐败会体现出来(注:corruption,意为腐败,类似代码的bad smell,指程序中出现的问题或bug)。

不能只是捕捉ThreadDeath异常并修复损坏的对象吗?

从理论上讲,也许可以,但这会_使编写正确的多线程代码的任务大大复杂化_。该任务几乎是无法克服的,原因有两个:

  1. 线程_几乎可以在任何地方_引发ThreadDeath异常 。考虑到这一点,必须仔细研究所有同步的方法和块。
  2. ThreadDeath从第一个(在catchor finally子句中)清除时,线程可以引发第二个异常。必须重复进行清理,直到成功。确保该代码很复杂。

如何停止等待较长时间的线程(例如,等待输入)?

这就是Thread.interrupt方法的用途。 可以使用上面展示的相同的“基于状态”的信令机制,但是状态更改(在前面的示例中,_blinker = null_)之后可以调用Thread.interrupt来中断等待:

 public void stop() {
        Thread moribund = waiter;
        waiter = null;
        moribund.interrupt();
    } 

为了使该技术起作用,至关重要的是,任何捕获中断异常并且不准备处理中断异常的方法都必须立即重新声明该异常。 我们说重新声明而不是重新抛出,因为并非总是可能重新抛出异常。 如果未声明捕获InterruptedException的方法引发此(checked)Exception,则它应使用下面的提示“重新中断自身”:

Thread.currentThread().interrupt(); 

这样可以确保线程尽快引发InterruptedException

如果线程不响应Thread.interrupt怎么办?

在某些情况下,您可以使用特定于应用程序的技巧。 例如,如果某个线程正在一个已知的套接字上等待,则可以关闭该套接字以使该线程立即返回。 不幸的是,一般来说,真的没有什么技术能奏效。 应该注意的是,在所有等待线程不响应Thread.interrupt的情况下,它也不响应Thread.stop 这样的情况包括故意的拒绝服务攻击,以及Thread.stopThread.interrupt无法正常工作的I / O操作。(_注释_:这段的个人理解,如果程序不响应Thread.interrupt,那就没办法了,自己去想别的方法解决吧。所以还是尽量使用推荐的方法来控制线程停止与运行)

为什么不赞成使用Thread.suspendThread.resume

Thread.suspend本质上容易死锁。 如果目标线程在挂起时,在监视器上持有一个保护关键系统资源的锁,则在恢复目标线程之前,没有线程可以访问该资源。 如果将恢复目标线程的线程在调用resume之前尝试锁定此监视器,则会导致死锁。 这种僵局通常表现为“冻结”进程。

应该使用什么代替Thread.suspendThread.resume

Thread.stop一样,谨慎的方法是让“目标线程”轮询一个指示线程所需状态(活动或挂起)的变量。 当所需的状态被挂起时,线程使用Object.wait等待。 恢复线程后,将使用Object.notify通知目标线程。

例如,假设您的程序包含以下“鼠标按下”事件处理程序,该事件处理程序的功能是可以切换blinker线程的状态:

 private boolean threadSuspended;

    Public void mousePressed(MouseEvent e) {
        e.consume();

        if (threadSuspended)
            blinker.resume();
        else
            blinker.suspend();  // DEADLOCK-PRONE!容易出现死锁

        threadSuspended = !threadSuspended;
    } 

您可以通过将上面的事件处理程序替换为下面的代码来避免使用Thread.suspendThread.resume

 public synchronized void mousePressed(MouseEvent e) {
        e.consume();

        threadSuspended = !threadSuspended;

        if (!threadSuspended)
            notify();
    } 

并添加下面的代码到 "run loop":

 synchronized(this) {
        while (threadSuspended)
            wait();
    } 

wait方法抛出InterruptedException,因此它必须在try ... catch子句中。 可以将其与sleep放在同一个的子句中。 该检查应在sleep之后(而不是在sleep之前),以便在线程“resumed”时立即重新绘制窗口。 生成的run方法如下:

 public void run() {
        while (true) {
            try {
                Thread.currentThread().sleep(interval);

                synchronized(this) {
                    while (threadSuspended)
                        wait();
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    } 

请注意,“鼠标按下”方法中的notify和run方法中的wait在同步块(synchronized)内部。这种做法是编程语言要求的,并确保wait和notify正确地按顺序执行。 实际上,这消除了可能导致“已暂停”线程错过通知并无限期保持暂停的竞争条件。

随着平台的成熟,尽管Java同步(synchronized)的性能开销在降低,但它永远不会“免费”(注:免费指几乎可以忽略的性能开销)。 一个简单的技巧可以用来删除我们添加到“运行循环”的每个迭代中的同步。 所添加的同步块被稍微复杂一点的代码所代替,仅当线程实际上已被挂起时才进入同步块:

 if (threadSuspended) {
        synchronized(this) {
            while (threadSuspended)
                wait();
        }
    } 

在没有显式同步的情况下,必须将threadSuspended设置为volatile,以确保及时传达suspend-request。

最终生成的run方法为:

 private boolean volatile threadSuspended;

    public void run() {
        while (true) {
            try {
                Thread.currentThread().sleep(interval);

                if (threadSuspended) {
                    synchronized(this) {
                        while (threadSuspended)
                            wait();
                    }
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    } 

可以结合两种技术来产生可以安全地“停止”或“暂停”的线程吗?

可以的, 这相当简单。 一个微妙之处是目标线程可能在另一个线程试图将其停止时已被挂起。 如果stop方法仅将状态变量(blinker)设置为null,则目标线程将保持挂起状态(在监视器上等待),而不是应有的退出。 如果重新启动了applet,则多个线程可能最终会同时在监视器上等待,从而导致行为不稳定。

要纠正这种情况,stop方法必须确保目标线程在挂起后立即恢复。 目标线程恢复后,必须立即识别出它已停止,并正常退出。 这是生成的run和stop方法的外观:

 public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try {
                thisThread.sleep(interval);

                synchronized(this) {
                    while (threadSuspended && blinker==thisThread)
                        wait();
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

    public synchronized void stop() {
        blinker = null;
        notify();
    } 

如上所述,如果stop方法调用Thread.interrupt,它也不必调用notify,但仍必须同步。 这样可以确保目标线程不会由于竞争条件而错过中断。

关于 Thread.destroy 方法

Thread.destroy从未真正被实现。 如果实现了它,使用Thread.suspend容易发生死锁。 (实际上,它与Thread.suspend大致等效,没有后续的Thread.resume的可能性。)我们目前不实现它,但也不会弃用它(将来将阻止其实现)。 尽管肯定会发生死锁,但有人认为,在某些情况下,某个程序愿意冒着deadlock的风险也不想直接退出。

Java8中的destroy方法源码:

 /**
     * Throws {@link NoSuchMethodError}.
     * 此方法最初设计为在不进行任何清理的情况下销毁此线程。
     * 它所持有的任何监视器都将保持锁定状态。
     * 但是,该方法从未实现。如果要实现,则将以{@link #suspend}的方式发生死锁。
     * 如果目标线程在被销毁时持有一个保护关键系统资源的锁,则没有线程可以再次访问该资源。 
     * 如果另一个线程曾尝试锁定此资源,则会导致死锁。这种死锁通常表现为“冻结”进程。
     */
    @Deprecated
    public void destroy() {
        throw new NoSuchMethodError();
    } 
收藏
评论区

相关推荐

1 Java内存区域与内存溢出异常
1 java虚拟机对内存的管理 java虚拟机在执行java程序的时候把内存分为若干个不同的区,这些区各自有不同的用处,以及创建和销毁时间. 有的区随着虚拟机的启动而启动,有的区则依赖用户线程的启动和结束而启动和结束. 根据java虚拟机规范,java虚拟机将内存分为下面几个部分:如下图 image(https://imghelloworld.o
.NET C#到Java没那么难,DB篇
.NET C到Java没那么难,DB篇 .NET C到Java没那么难,DB篇 前言 .NET C到Java没那么难,都是面向对象的语言,而且语法还是相似的,先对比一下开发环境,再到Servlet,再到
.NET C#到Java没那么难,MVC篇
.NET C到Java没那么难,MVC篇 .NET C到Java没那么难,MVC篇 最典型的JAVA MVC就是JSP servlet javabean的模式。比较好的MVC,老牌的有Struts、
.NET C#到Java没那么难,Servlet篇
.NET C到Java没那么难,Servlet篇 .NET C到Java没那么难,Servlet篇 前言 .NET C到Java没那么难,都是面向对象的语言,而且语法还是相似的,先对比一下开发
Linux下安装jdk
一 、安装前 java 1.查看是否已安装JDK yum list installed |grep java 2.卸载CentOS系统Java环境 yum y remove java1.8.0openjdk 表示卸载所有openjdk相关文件输入 yum y remove tzdatajava.noarch 卸载t
Java中的浮点数四舍五入到小数点后2位的几种方法
前言 四舍五入到2或3个小数位是我们Java程序员日常开发中肯定会遇到。幸运的是,Java API提供了几种在Java中舍入数字的方法 我们可以使用Math.round(),BigDecimal或DecimalFormat将Java中的任何浮点数四舍五入到n个位置。我个人更喜欢使用BigDecimal在Java中四舍五入任何数字,因为它具有便捷的API并
Groovy初探
开始之前 了解本教程的主要内容,以及如何从中获得最大收获。 关于本教程 如果现在有人要开始完全重写 Java,那么 Groovy 就像是 Java 2.0。Groovy 并没有取代 Java,而是作为 Java 的补充,它提供了更简单、更灵活的语法,可以在运行时动态地进行类型检查。您可以使用 Groovy 随意编写 Java 应用程序,连接 Java
《java 核心技术》卷1 学习 概述 第一章Java程序设计概述
从浅面了解Java 1.Java 在语言得地位 现在有所下降 但仍是老大哥 所以值得学习 2.Java特性 1.简单性:从一方面来说 Java可以支持在小型机器上运行 必定不是很复杂得,所以上手不难 2.面向对象:Java有相比于其他的语言 更简单得接口
Java里面的十万个为什么
Java里面的十万个为什么 1.不是说 JVM 是运行 Java 程序的虚拟机吗?那 JRE 和 JVM 的关系是怎么样的呢?简单地说,JRE 包含 JVM 。JVM 是运行 Java 程序的核心虚拟机,而运行 Java 程序不仅需要核心虚拟机,还需要其他的类加载器,字节码校验器以及大量的基础类库。JRE 除包含 JVM 之外,还包含运行 Java 程序的其
Java学习路线
阶段一 (夯实基础)Java基础语法学习目标:1.熟悉Java等基本概念2.掌握Eclipse/IDEA集成开发工具的安装、配置和应用3.熟悉Java基本语法、基本类型、运算符和表达式4.掌握分支、循环逻辑语句、数组等知识的应用知识点列表:JDK、JRE、JVM基本概念Java环境搭建和配置安装和使用Eclipse/IDEA开发环境Java基本数据类型变量,
Java开发面试高频考点学习笔记(每日更新)
Java开发面试高频考点学习笔记(每日更新) 1.深拷贝和浅拷贝 2.接口和抽象类的区别 3.java的内存是怎么分配的 4.java中的泛型是什么?类型擦除是什么? 5.Java中的反射是什么 6.序列化与反序列化 7.Object有哪些方法? 8.JVM内存模型 9.类加载机制 10.对象的创建和对象的布局 11.Java的四种引用
2021年度最全面JVM虚拟机,类加载过程与类加载器
前言类装载器子系统是JVM中非常重要的部分,是学习JVM绕不开的一关。一般来说,Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表
秋招已经开始准备了!【Java面试题】最新Java开发岗面试知识笔记
在最近两个月不断的面试中,我分类总结了 Java 开发岗位面试中的一些知识点。主要包括以下几个部分: 1. Java 基础知识点 2. Java 常见集合 3. 高并发编程(JUC 包) 4. JVM 内存管理 5. Java 8 知识点 6. 网络协议相关 7. 数据库相关 8. MVC 框架相关 9. 大数据相关 10. Linux 命令相关面试,
2021年度最全面JVM虚拟机,类加载过程与类加载器
前言类装载器子系统是JVM中非常重要的部分,是学习JVM绕不开的一关。一般来说,Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表
2021年Java常见面试题目,100%好评!
专题1:JavaOOP 1、什么是B/S架构?什么是C/S架构 2、Java都有哪些开发平台? 3、什么是JDK?什么是JRE? 4、Java语言有哪些特点 5、面向对象和面向过程的区别 6、什么是数据结构? 7、Java的数据结构有哪些? 8、什么是OOP? 9、类与对象的关系? 10、Java中有几种数据类型