JAVA多线程基础篇--线程终止(Interrupt与stop)

1.概述

日常业务中,可能会遇到这样一个场景:终止一个正在运行的线程。停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。优雅地停止线程是java开发中比较重要的技术点,因此需要一些技巧。本文将基于多线程停止的几种方式来进行分析,并对比各种方式的优劣。

2.线程停止的几种方法

首先要了解JAVA中终止正在运行线程的三种方法:

2.1 调用Thread.stop()方法来强行终止线程

2.1.1 一个小案例

这种方式是不被推荐的,因为stop()、suspend()与resume()都是作废过期的方法,使用这些方法会带来不可预料的后果。尤其是stop()方法,会强行终止线程,可能造成数据不一致的后果,最终导致程序执行流程异常。
首先看一个线程终止的案例:

import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Slf4j
public class ThreadStopRunnable extends Thread {

    @Override
    public void run() {
        while (true) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
            Date currentDate = new Date();
            String date = simpleDateFormat.format(currentDate);
            log.error("当前线程:{},正在运行,时间为:{}", Thread.currentThread().getName(), date);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                log.error("线程休眠异常:{}", e);
            }
        }
    }
}

线程执行代码如下:

import java.util.concurrent.TimeUnit;

@Slf4j
public class ThreadStop {
    public static void main(String[] args) {
        ThreadStopRunnable threadStopRunnable = new ThreadStopRunnable();
        Thread thread = new Thread(threadStopRunnable);
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (Exception e) {
            log.error("主线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
        }
        thread.stop();
        log.info("主线程:{}结束", Thread.currentThread().getName());
    }
}

程序运行结果如下:
在这里插入图片描述
上述第一段代码中设置了一个死循环,每隔2s打印一下当前线程的运行状态和时间;第二段代码为线程执行代码,这段代码休眠10s后调用了stop()方法终止了第一段代码的执行。由上述代码可知,stop()方法确实能够终止线程的运行。但是stop()方法是一个非常暴力的方法,需要谨慎使用。

2.1.2 stop()方法与java.lang.ThreadDeath异常

调用stop()方法会抛出java.lang.ThreadDeath异常,通常情况下不需要显示捕捉该异常,该异常是 Error 的子类而不是 Exception 的子类,单纯通过捕获所有Exception异常是无法捕捉该异常的。具体该异常捕获情况如下:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ThreadStopRunable extends Thread {

    @Override
    public void run() {
        try {
            this.stop();
        } catch (Error e) {
            log.error("线程已终止:{}", e.toString());
        }
    }
}

线程执行代码如下:

@Slf4j
public class ThreadStop {
    public static void main(String[] args) {
        ThreadStopRunable threadStop1 = new ThreadStopRunable();
        threadStop1.start();
    }
}

执行结果如下所示:
请添加图片描述

2.2 使用标记位来退出线程

这种方式主要实现方式是:定义一个标记位,当线程run()方法运行完成之后,修改标记位状态为false,进而达到终止线程的目标。修改上述代码如下:

import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Slf4j
public class ThreadStopRunnable extends Thread {

    public boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
            Date currentDate = new Date();
            String date = simpleDateFormat.format(currentDate);
            log.error("当前线程:{},正在运行,时间为:{}", Thread.currentThread().getName(), date);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                log.error("线程休眠异常:{}", e);
            }
        }
    }

    public void setFlag() {
        this.flag = false;
        log.info("setFlag:{}", false);
    }
}

线程执行代码如下:

@Slf4j
public class ThreadStop {
    public static void main(String[] args) {
        ThreadStopRunnable thread = new ThreadStopRunnable();
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (Exception e) {
            log.error("主线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
        }
        //修改线程状态为false,终止线程执行
        thread.setFlag();
        log.info("主线程:{}结束", Thread.currentThread().getName());
    }
}

执行结果如下:
在这里插入图片描述
主线程执行后,运行执行线程,主线程休眠10s后,调用执行线程终止方法,子线程运行结束。

2.3 使用interrupt()方法终止线程

2.3.1 无法停止线程

interrupt()方法可以停止线程,但interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。观察并运行以下代码:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Slf4j
public class ThreadStopRunnable extends Thread {

    public boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:MM:ss");
            Date currentDate = new Date();
            String date = simpleDateFormat.format(currentDate);
            log.info("当前线程:{},正在运行,时间为:{}", Thread.currentThread().getName(), date);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (Exception e) {
                log.error("线程休眠异常:{}", e);
            }
        }
    }
}

主线程如下:

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;

@Slf4j
public class ThreadStop {
    public static void main(String[] args) {
        ThreadStopRunnable thread = new ThreadStopRunnable();
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (Exception e) {
            log.error("主线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
        }
        //调用interrupt方法
        thread.interrupt();
        log.info("主线程:{}结束", Thread.currentThread().getName());
    }
}

程序运行结果如下:
在这里插入图片描述
由上述结果可知,当调用interrupt()方法之后,程序并没有立刻停止,主线程main结束后,子线程依旧保持运行。interrupt()的作用是中断此线程,注意此线程不一定是当前线程,而是值调用该方法的Thread实例所代表的线程,调用interrupt方法不会真正的结束线程,在当前线程中打上一个停止的标记,线程仍然会继续运行。由上图可知,结果中抛出了一个sleep interrupted异常,这里涉及一个知识点:如果在sleep状态下停止某一线程(调用interrupt方法),线程会抛出异常,并且清除停止状态值,使之变成false

2.3.2 判断线程停止状态

Thread类提供了interrupted()方法来测试当前线程是否被中断(检查该线程的中断标志),调用该方法会返回一个boolean值并清除中断的状态,第二次调用时中断状态已经被清除,所以会返回false。下面看一段代码:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Slf4j
public class ThreadInterrupted {

    public static void main(String[] args) {
        Interrupted thread = new Interrupted();
        thread.setName("Interrupted-thread");
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
        }
        thread.interrupt();
        //thread.isInterrupted方法是检查Interrupted是否被打上停止的标记
        log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
        //Thread.interrupted方法是检查主线程是否打上停止的标记
        log.info("线程:{},是否中断:{}", Thread.currentThread().getName(), Thread.interrupted());
    }
}


@Slf4j
class Interrupted extends Thread {

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        while (true) {
            Date date = new Date();
            String format = simpleDateFormat.format(date);
            log.info("当前时间:{}", format);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
            }
        }
    }
}

上述程序的运行结果如下:

在这里插入图片描述

interrupted()与isInterrupted()的唯一区别在于:前者会读取并清除中断状态,后者仅读取状态。

为了验证上述这一结论,首先看一段Thread的源码:

 /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }

这是interrupted()在Thread类中的源码,它的上面有一段注释,翻译后该意思如下:

测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

为了验证上述这段话,我们写一段代码来测试一下:

@Slf4j
public class ThreadInterrupted {

    public static void main(String[] args) {
        log.info("当前线程:{},启动成功!");
        Thread.currentThread().interrupt();
        log.info("当前线程:{},是否终止:{}",Thread.currentThread().getName(),Thread.interrupted());
        log.info("当前线程:{},是否终止:{}",Thread.currentThread().getName(),Thread.interrupted());
    }
}

上述代码的运行结果为:
在这里插入图片描述
上述结果印证了interrupted()方法具有清除状态的功能,所以第二次打印显示的值为false。
同理,我们也需要验证一下isinterrupted()方法是否会清除状态?验证代码如下:

@Slf4j
public class ThreadInterrupted {

    public static void main(String[] args) {
        Interrupted thread = new Interrupted();
        thread.setName("Interrupted-thread");
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
        }
        thread.interrupt();
        log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
        log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
    }
}


@Slf4j
class Interrupted extends Thread {

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        while (true) {
            Date date = new Date();
            String format = simpleDateFormat.format(date);
            log.info("当前时间:{}", format);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
            }
        }
    }
}

运行结果如下所示:
在这里插入图片描述
从结果中可以看到,方法 isInterrupted()并未清除状态标志,所以打印了两个true。
由上述一系列代码可以得出以下结论:

this.interrupted() :测试当前线程是否已经是中断状态,执行后具有将状态标志置清除为false的功能。
this.isInterrupted() :测试线程Thread对象是否已经是中断状态,但不清除状态标志。

2.3.3 如何正确停止线程

上面讲述了关于如何判断线程是否处于中断状态,并且interrupte()方法不能直接终止线程,那么要如何安全终止线程呢?这里可以使用抛出异常法来终止线程。具体可以参考下面案例:

@Slf4j
public class ThreadInterrupted {

    public static void main(String[] args) {
        Interrupted thread = new Interrupted();
        thread.setName("Interrupted-thread");
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            log.error("线程:{},休眠异常:{}", Thread.currentThread().getName(), e);
        }
        thread.interrupt();
        log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
        log.info("线程:{},是否中断:{}", thread.getName(), thread.isInterrupted());
    }
}


@Slf4j
class Interrupted extends Thread {

    @SneakyThrows
    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        while (true) {
            if (this.isInterrupted()) {
                log.error("线程:{},已经被终止",Thread.currentThread().getName());
                throw new InterruptedException("线程被终止");
                //return;
            }
            Date date = new Date();
            String format = simpleDateFormat.format(date);
            log.info("当前时间:{}", format);
        }
    }
}

上述线程运行结果如下:
在这里插入图片描述
这种方式主要在子线程中加了一个状态检测判断,如果判断该线程的中断状态为true,则直接抛出异常,进而达到终止线程的目的。这种方式一旦检测到了中断状态,就不会再向下执行该线程的代码,保证了安全性。事实上,如果不想抛出异常,可以在检测线程中断状态为true后,直接利用return终止线程,也就是将上述代码throw new InterruptedException("线程被终止");换成return;

3.注意事项

3.1 使用stop()方法造成读写不一致案例

为了验证stop()方法的不安全型,编写以下案例加以验证。
首先编写一个User类,包含两个属性:id和userName。

import lombok.Data;

@Data
public class User {

    public User() {
        this.id = 0;
        this.userName="0";
    }

    private Integer id;

    private String userName;

}

其次编写一个写线程:

import java.util.concurrent.TimeUnit;

@Slf4j
public class WriteThread extends Thread {

    private User user;

    public WriteThread(User user) {
        this.user = user;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (user) {
                int currentTime = (int) (System.currentTimeMillis() / 1000);
                user.setId(currentTime);
                try {
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    log.error("线程休眠异常:{}", e);
                }
                user.setUserName(String.valueOf(currentTime));
            }
            Thread.yield();
        }
    }
}

再编写一个读线程:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ReadThread extends Thread {

    private User user;

    public ReadThread(User user) {
        this.user = user;
    }

    @Override
    public void run() {
        while (true) {
            synchronized (user) {
                while (!user.getUserName().equals(user.getId().toString())) {
                    log.info("userId:{},userName:{}", user.getId(), user.getUserName());
                }
            }
            Thread.yield();
        }
    }
}

最后编写主线程代码:

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

@Slf4j
public class RunnableThread {
    public static void main(String[] args) {
        User user = new User();
        ReadThread readThread = new ReadThread(user);
        readThread.start();
        while (true) {
            WriteThread writeThread = new WriteThread(user);
            writeThread.start();
            try {
                TimeUnit.MICROSECONDS.sleep(200);
            } catch (InterruptedException e) {
                log.error("线程休眠异常:{}", e);
            }
            writeThread.stop();
        }
    }
}

可得到如下结果:
在这里插入图片描述

4.小结

1.stop()方法是一个废弃的方法,会造成数据不一致问题,建议不要使用;
2.interrupted()与isInterrupted()的唯一区别在于:前者会读取并清除中断状态,后者仅读取状态;
3.利用isInterrupted()方法判断线程运行状态,再结合抛出异常法,可以有效并安全终止线程。

4.参考文献

1.《JAVA多线程编程核心技术》-高洪岩著
2.https://juejin.cn/post/7019594104229068831

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值