为什么不推荐使用 stop、suspend 方法中断线程?

简介: 我们知道像stop、suspend这几种中断或者阻塞线程的方法在较高java版本中已经被标记上了@Deprecated过期标签,那么为什么她们曾经登上了java的历史舞台而又渐渐的推出了舞台呢?到底是人性的扭曲还是道德的沦丧呢,亦或是她们不思进取被取而代之呢,如果是被取而代之,那么取而代之的又是何方人也,本文我们将一探究竟。

一、stop的落幕

首先stop方法的作用是什么呢,用java源码中的一句注释来了解一下:Forces the thread to stop executing.,即强制线程停止执行,'Forces’似乎已经透漏出了stop方法的蛮狠无理。

那么我们再看看java开发者是怎们解释stop被淘汰了的:

This method is inherently unsafe. Stopping a thread with Thread.stop causes it to unlock all of the monitors that it has locked (as a natural consequence of the uncheckedThreadDeath exception propagating up the stack). If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to other threads, potentially resulting in arbitrary behavior. Many uses of stop should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), the interrupt method should be used to interrupt the wait.

我们从中可以看出以下几点:

  1. stop这种方法本质上是不安全的
  2. 使用Thread.stop停止线程会导致它解锁所有已锁定的监视器,即直接释放当前线程已经获取到的所有锁,使得当前线程直接进入阻塞状态

我们举例来看一下上边提到的两点:

public static void main(String[] args) throws InterruptedException {
       Object o1=new Object();
       Object o2=new Object();
       Thread t1=new Thread(()->{
             synchronized (o1)
             {
                 synchronized (o2)
                 {
                     try {
                         System.out.println("t1获取到锁");
                         Thread.sleep(5000);
                         System.out.println("t1结束");
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             }
       });
       t1.start();
       Thread.sleep(1000);
       Thread t2=new Thread(()->{
           synchronized (o1)
           {
               synchronized (o2)
               {
                   try {
                       System.out.println("t2获取到锁");
                       Thread.sleep(5000);
                       System.out.println("t2结束");
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           }
       });
       t2.start();
       t1.stop();
   }

运行结果:

微信图片_20210922001904.jpg

可以看到,当线程t1在获取到o1和o2两个锁开始执行,在还没有执行结束的时候,主线程调用了t1的stop方法中断了t1的执行,释放了t1线程获取到的所有锁,中断后t2获取到了o1和o2锁,开始执行直到结束,而t1却夭折在了sleep的时候,sleep后的代码没有执行。

因此使用stop我们在不知道线程到底运行到了什么地方,暴力的中断了线程,如果sleep后的代码是资源释放、重要业务逻辑等比较重要的代码的话,亦或是其他线程依赖t1线程的运行结果,那直接中断将可能造成很严重的后果。

那么不建议使用stop中断线程我们应该怎么去优雅的结束一个线程呢,我们可以存java开发者的注释中窥探到一种解决方案:

Many uses of stop should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), the interrupt method should be used to interrupt the wait.

可以看到java开发者推荐我们使用以下两种方法来优雅的停止线程。

1.定义一个变量,由目标线程去不断的检查变量的状态,当变量达到某个状态时停止线程。

代码举例如下:

volatile static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
       Object o1=new Object();
       Thread t1=new Thread(()->{
             synchronized (o1)
             {
                 try {
                     System.out.println("t1获取到锁");
                     while (!flag)
                         Thread.sleep(5000);//执行业务逻辑
                     System.out.println("t1结束");
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
       });
       t1.start();
       Thread.sleep(1000);
       Thread t2=new Thread(()->{
           synchronized (o1)
           {
               try {
                   System.out.println("t2获取到锁");
                   Thread.sleep(5000);//执行业务逻辑
                   System.out.println("t2结束");
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
       t2.start();
       flag=true;
   }

运行结果:

微信图片_20210922001904.jpg

2.使用interrupt方法中断线程。

代码举例如下:

public static void main(String[] args) throws InterruptedException {
       Object o1=new Object();
       Thread t1=new Thread(()->{
             synchronized (o1)
             {
                 System.out.println("t1获取到锁");
                 while (!Thread.currentThread().isInterrupted()) {
                     for (int i = 0; i < 100; i++) {
                         if(i==50)
                             System.out.println();
                         System.out.print(i+" ");
                     }
                     System.out.println();
                 }
                 System.out.println("t1结束");
             }
       });
       t1.start();
       Thread t2=new Thread(()->{
           synchronized (o1)
           {
               try {
                   System.out.println("t2获取到锁");
                   Thread.sleep(5000);//执行业务逻辑
                   System.out.println("t2结束");
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
       t2.start();
       t1.interrupt();
   }

运行结果:

微信图片_20210922001953.jpg

我们用while (!Thread.currentThread().isInterrupted())来不断判断当前线程是否被中断,中断的话则让线程自然消亡并释放锁。可以看到调用interrupt方法后并不会像stop那样暴力的中断线程,会等到当前运行的逻辑结束后再检查是否中断,非常的优雅。

注:运行举例代码可能不会打印出数字,这是因为t1线程运行到while(!Thread.currentThread().isInterrupted())时,主线程已经调了interrupt方法,因此多次运行可能会打印出数字。

二、suspend的落幕

suspend方法的作用是挂起某个线程直到调用resume方法来恢复该线程,但是调用了suspend方法后并不会释放被挂起线程获取到的锁,正因如此就给suspend和resume这哥俩贴上了容易引发死锁的标签,当然这也正是导致suspend和resume退出历史舞台的罪魁祸首。同样我们看看java开发者为suspend的淘汰给出的理由:

This method has been deprecated, as it is inherently deadlock-prone. If the target thread holds a lock on the monitor protecting a critical system resource when it is suspended, no thread can access this resource until the target thread is resumed. If the thread that would resume the target thread attempts to lock this monitor prior to calling resume, deadlock results. Such deadlocks typically manifest themselves as “frozen” processes.

从中我们可以得出以下结论:

  1. suspend具有天然的死锁倾向
  2. 当某个线程被suspend后,该线程持有的锁不会被释放,其他线程也就不能访问这些资源
  3. suspend某个线程后,如果在resume的过程中出现异常导致resume方法执行失败,则lock无法释放,导致死锁

接下来模拟一下由suspend引起的死锁场景,Talk is cheap,show my code:

public static void main(String[] args) throws InterruptedException {
       Object o1=new Object();
       Object o2=new Object();
       Thread t1=new Thread(()->{
             synchronized (o1)
             {
                 System.out.println("t1获取到o1锁开始执行");
                 try {
                     Thread.sleep(5000);//模拟执行业务逻辑
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("t1执行结束");
             }
       });
       t1.start();
       Thread t2=new Thread(()->{
           synchronized (o2)
           {
               System.out.println("t2获取到o2开始执行");
               try {
                   Thread.sleep(2000);//执行耗时业务
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               synchronized (o1)
               {
                   System.out.println("t2获取到o1锁开始继续执行");
               }
               System.out.println("t2执行结束");
           }
       });
       t2.start();

       Thread.sleep(1000);
       t1.suspend();
       //假设抛出了一个未知异常
       int i=1/0;
       t1.resume();
   }

运行结果:

微信图片_20210922002202.jpg

可以看到,整个程序卡的死死的,在调用resume恢复t1线程之前抛出了一个未知异常,导致t1一直挂起进而无法释放o1锁,而t2需要获取到o1锁后才能继续执行,但苦苦等待,奈何o1被t1拿捏的死死的,从此整个程序就陷入了无尽的等待中----死锁。

目录
相关文章
|
4月前
|
存储 Oracle Java
|
6月前
|
Java
创建线程的方法
Java中实现多线程有四种方式:1. 继承Thread类,简单但占用继承机会,耦合度高;2. 实现Runnable接口,推荐方式,任务与线程解耦,支持Lambda;3. 实现Callable接口配合FutureTask,可获取返回值和异常;4. 使用线程池(ExecutorService),企业推荐,管理线程生命周期,提升性能,支持多种线程池类型。
156 1
|
7月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
485 5
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
Java 程序员 调度
【JavaEE】线程创建和终止,Thread类方法,变量捕获(7000字长文)
创建线程的五种方式,Thread常见方法(守护进程.setDaemon() ,isAlive),start和run方法的区别,如何提前终止一个线程,标志位,isinterrupted,变量捕获
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
272 3
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
154 2
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
203 1
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
345 1
|
Java
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅
在Java多线程编程中,`wait()`和`notify()`方法的相遇如同一场奇妙的邂逅。它们用于线程间通信,使线程能够协作完成任务。通过这些方法,生产者和消费者线程可以高效地管理共享资源,确保程序的有序运行。正确使用这些方法需要遵循同步规则,避免虚假唤醒等问题。示例代码展示了如何在生产者-消费者模型中使用`wait()`和`notify()`。
134 1

热门文章

最新文章