0%

Java 并发 - 多线程:如何停止一个线程

1 简介

对于一个线程要如何去停止呢?还有不同情况下的线程要如何停止呢?停止一个线程是什么意思?就是让这个线程在它进行任务处理的时候进行停止,停掉当前的操作,之前有学习到一个Thread.stop()方法,好像已经被废弃了,是不安全的一个方法,那么除了这个方法,还有其他什么办法吗?

总的来说,Java有如下几种方法去停止线程:

  1. 使用stop方法强行终止,但是不推荐这个方法,因为stop是过期作废的方法
    • stop()方法作废的原因:如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁,导致数据得不到同步的处理,出现数据不一致的问题
  2. 使用interrupt方法中断线程
    • 停止不了的线程,interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程
    • 可以用查看线程是否中断,并抛出异常的方式来停止执行线程中的函数
    • 如果线程在sleep()函数的时间范围内被interrupted就会中断线程,置状态位为false并抛出sleep interrupted异常。
  3. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
    • 使用return,配合interrupt标志可以直接中断线程
    • 但是还是建议使用异常中断线程,因为可以使用catch向上抛出异常,从而使线程停止事件得以传播
  4. 线程池使用shutDownAll()
  5. Looper的quit方法或quitSafely方法

2 总体方法详细介绍

2.1 interrupt的特性

interrupt的特性是什么?interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程

那样的话会带来什么后果呢?看一下下面的Demo的运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyThread extends Thread {
public void run(){
super.run();
// 线程不断的进行打印
for(int i=0; i<500000; i++){
System.out.println("i="+(i+1));
}
}
}

public class Run {
public static void main(String args[]){
Thread thread = new MyThread();
thread.start();
try {
Thread.sleep(2000);
// 注意我这里使用了interrupt方法了!
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

打印输出如下:

1
2
3
4
5
6
7
8
...
i=499994
i=499995
i=499996
i=499997
i=499998
i=499999
i=500000

你会发现他会不停的打印输出,并没有停止,我用了interrupt方法了啊?哪里出问题了??

其实原理上面都已经讲了,只是在当前线程中打了一个停止标志,没有真的停止线程!

那我要这interrupt有何用???冷静,还是有用的,配合其他方法使用不就可以了吗?

打出两套组合拳:

  1. 使用interrupt+状态停止判断+抛异常
  2. 使用interrupt+状态停止判断+退出标志(例如:return)

具体组合拳如何打法如以下两个小点所示。


2.2 停止状态判断

线程与状态是密不可分的,即我们要去停止一个线程,我们也要去判断当前是什么状态,执行了想要的停止操作之后,线程是否马上就变为停止状态了,所以如何去判断线程是否为中断状态是很关键的一点,Thread.java类中提供了两种方法:

  1. this.interrupted(): 测试当前线程是否已经中断,同时线程的中断状态由该方法清除;
  2. this.isInterrupted(): 测试线程是否已经中断,同时不会清除标志位;

两种方法都能用?那这两种方法的区别在哪里?这也是我的一个疑问了。

其实主要的区别有两个:

  1. this.interrupted() 测试的是当前线程,当前线程指的是什么?就是指运行this.interrupted()方法的线程。
  2. 连续调用两次this.interrupted() 会进行状态清除,而连续调用多次this.isInterrupted()不会发生状态清除。

先讲讲第一个区别,当前线程具体是如何的当前!

验证Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 通过继承Thread 方式实现一个线程
public class MyThread extends Thread {
public void run(){
super.run();
for(int i=0; i<500000; i++){
i++;
}
}
}

public class Run {
// main 主线程
public static void main(String args[]){
Thread thread = new MyThread();
thread.start();
try {
Thread.sleep(2000);
// 此处就调用了interrupt 方法试图去停止线程了
thread.interrupt();
// 注意此处调用了两次 interrupted
System.out.println("stop 1??" + thread.interrupted());
System.out.println("stop 2??" + thread.interrupted());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行结果如下所示:

1
2
stop 1??false
stop 2??false

在上面的代码中我们可以看见,我们thread 对象上进行调用了thread.interrupt();,接着又连续使用了两次的thread.interrupted()); 去进行thread对象所代表的线程是否已经停止了,但是结果验证发现thread对象所代表的线程是未停止的,也就是不生效???

其实是thread.interrupt();是有对Thread对象生效的,但是thread.interrupted()); 并不是针对thread 对象的,其实他所针对的是main,当前线程是main!这就是为什么打印了两个false,因为当前线程main从未中断过。

如何让thread线程进行thread.interrupt();的生效呢?我的想法就是在MyThread里面进行使用this.interrupted(),则能够进行判断了。

那么如何让main线程产生中断效果?答案就是使用:Thread.currentThread().interrupt();

interrupted 的验证

查阅相关资料,发现这个方法的解释为:官方帮助文档中对interrupted方法的解释:测试当前线程是否已经中断。线程的中断状态由该方法清除。 换句话说,如果连续两次调用该方法,则第二次调用返回false。

注意:当前线程!!!

以下为interrupted方法的测试Demo:

1
2
3
4
5
6
7
8
public class InterrruptedTest {
public static void main(String args[]){
Thread.currentThread().interrupt();
System.out.println("stop 1??" + Thread.interrupted());
System.out.println("stop 2??" + Thread.interrupted());
System.out.println("End");
}
}

输出如下:

1
2
3
stop 1??true
stop 2??false
End

方法interrupted()的确判断出当前线程是否是停止状态。但为什么第2个布尔值是false呢?官方帮助文档中对interrupted方法的解释:测试当前线程是否已经中断。线程的中断状态由该方法清除。 换句话说,如果连续两次调用该方法,则第二次调用返回false。

isInterrupted 的验证

主要验证的就是isInterrupted这个方法不会去清除状态,就算是连续多次调用都不会清除状态。

测试Demo如下:

1
2
3
4
5
6
7
8
9
10
11
public class IsInterruptedTest {
public static void main(String args[]){
Thread thread = new MyThread();
thread.start();
// 此处调用了interrupt
thread.interrupt();
// 使用isInterrupted 进行验证线程是否为停止状态
System.out.println("stop 1??" + thread.isInterrupted());
System.out.println("stop 2??" + thread.isInterrupted());
}
}

打印结果如下:

1
2
stop 1??true
stop 2??true

可以看见isInterrupted 不会像interrupted 去清除状态,于是便打印了两个true。


2.3 使用抛异常的方法停止线程

在使用抛异常的方法来停止线程之前,可以看看如果不使用抛异常的方法来停止线程会产生什么后果?我们前面知道了,可以使用thread.interrupt() 方法去进行停止线程,然后再联系Thread.interrupted 方法区判断是否为停止状态,那么我们需要的就是是如果是停止状态,后面的代码不再运行就可以了。

验证Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyThread extends Thread {
public void run(){
super.run();
// for循环进行打印
for(int i=0; i<500000; i++){
// 判断是否为中断状态
if(this.interrupted()) {
System.out.println("线程已经终止, for循环不再执行");
break;
}
// 否则进行打印输出
System.out.println("i="+(i+1));
}
}
}

public class Run {
public static void main(String args[]){
// 申请线程并且开启线程
Thread thread = new MyThread();
thread.start();
try {
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

打印输出如下:

1
2
3
4
5
6
...
i=202053
i=202054
i=202055
i=202056
线程已经终止, for循环不再执行

上面的示例虽然停止了线程,但如果for语句下面还有语句,还是会继续运行的。看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyThread extends Thread {
public void run(){
super.run();
for(int i=0; i<500000; i++){
if(this.interrupted()) {
System.out.println("线程已经终止, for循环不再执行");
break;
}
System.out.println("i="+(i+1));
}
// 这里再来一句,按道理我是不想这里被执行到的
System.out.println("这是for循环外面的语句,也会被执行");
}
}

打印输出结果如下:

1
2
3
4
5
6
7
...
i=180136
i=180137
i=180138
i=180139
线程已经终止, for循环不再执行
这是for循环外面的语句,也会被执行

但是很不幸的是,并没有达到我们的目的,我们完成的仅仅只是for循环里面的结束而已,只是在for里面进行了一个判断中断标志位,并进行break而已,没有中断到我们的线程,那么我们该怎么做呢?

此时有一个好办法,就是使用抛异常的方法来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyThread extends Thread {
public void run(){
super.run();
try {
for(int i=0; i<500000; i++){
// 如果判断当前线程是为interrupt状态,则抛出一个中断异常
if(this.interrupted()) {
System.out.println("线程已经终止, for循环不再执行");
throw new InterruptedException();
}
System.out.println("i="+(i+1));
}
// 我们不希望这一句被执行到
System.out.println("这是for循环外面的语句,也会被执行");
} catch (InterruptedException e) {
// 使用catch进行异常捕获,如果捕获到异常,打印输出下面的话
System.out.println("进入MyThread.java类中的catch了。。。");
e.printStackTrace();
}
}
}

输出如下:

1
2
3
4
5
6
7
...
i=203798
i=203799
i=203800
线程已经终止, for循环不再执行
进入MyThread.java类中的catch了。。。
java.lang.InterruptedException at thread.MyThread.run(MyThread.java:13)

这样一来,发现其实我们想要的目的已经达到了,所以这个方案可行。


2.4 使用return 停止线程

将方法interrupt()与return结合使用也能实现停止线程的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyThread extends Thread {
public void run(){
while (true){
// 如果判断是线程停止状态,则调用return;
if(this.isInterrupted()){
System.out.println("线程被停止了!");
return;
}
// 否则打印输出当前时间
System.out.println("Time: " + System.currentTimeMillis());
}
}
}

public class Run {
public static void main(String args[]) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
Thread.sleep(2000);
// 此处调用了 interrupt 方法
thread.interrupt();
}
}

打印如下:

1
2
3
4
5
...
Time: 1467072288503
Time: 1467072288503
Time: 1467072288503
线程被停止了!

不过还是建议使用“抛异常”的方法来实现线程的停止,因为在catch块中还可以将异常向上抛,使线程停止事件得以传播。


2.5 sleep状态下如何停止线程

如果一个线程处于sleep()状态,我们去停止该线程,会发生什么?

以下我们进行了开启了一个线程,并且start了之后瞬间进入sleep(),然后我们再在主线程中进行一个thread.interrupt();看看会发生什么事情?

看看一下的测试Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread extends Thread {
public void run(){
super.run();
try {
System.out.println("线程开始。。。");
Thread.sleep(200000);
System.out.println("线程结束。");
} catch (InterruptedException e) {
System.out.println("在沉睡中被停止, 进入catch, 调用isInterrupted()方法的结果是:" + this.isInterrupted());
e.printStackTrace();
}
}
}
public class run {
public static void main(String[] args) {
Thread thread = new MyThread();
thread.start();
thread.interrupt();
}
}

运行结果如下:

1
2
3
4
5
线程开始。。。
在沉睡中被停止, 进入catch, 调用isInterrupted()方法的结果是:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.MyThread.run(MyThread.java:12)

从打印的结果来看, 如果在sleep状态下停止某一线程,会进入catch语句,并且清除停止状态值,使之变为false

那么,换一种方式!前面是先进入sleep(),然后再interrupt

如果我们让线程开始了之后,立马就使用interrupt方法,然后再进入sleep(),会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyThread extends Thread {
public void run(){
super.run();
try {
System.out.println("线程开始。。。");
for(int i=0; i<10000; i++){
System.out.println("i=" + i);
}
Thread.sleep(200000);
System.out.println("线程结束。");
} catch (InterruptedException e) {
System.out.println("先停止,再遇到sleep,进入catch异常");
e.printStackTrace();
}

}
}

public class Run {
public static void main(String args[]){
Thread thread = new MyThread();
thread.start();
// 注意在start之后就进行interrupt了!
thread.interrupt();
}
}

运行结果:

1
2
3
4
5
6
i=9998
i=9999
先停止,再遇到sleep,进入catch异常
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.MyThread.run(MyThread.java:15)

直观感觉就是,使用interrupt 之后,遇到了sleep还是一样会进入异常!可以得出的结果就是:如果线程在sleep()函数的时间范围内被interrupted就会中断线程,置状态位为false并抛出sleep interrupted异常


2.6 关于stop的几句唠叨

2.6.1 stop的暴力

使用stop方法非常暴力,直接粗暴!

Demo测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyThread extends Thread {
private int i = 0;
public void run(){
super.run();
try {
while (true){
System.out.println("i=" + i);
i++;
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public class Run {
public static void main(String args[]) throws InterruptedException {
Thread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.stop();
}
}

测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
i=0
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9

Process finished with exit code 0

2.6.2 stop与异常

调用stop()方法时会抛出java.lang.ThreadDeath异常,但是通常情况下,此异常不需要显示地捕捉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {

public static class MyThread extends Thread {
private int i = 0;
public void run(){
super.run();
try {
// 使用了stop
this.stop();
} catch (ThreadDeath e) { // 进行了异常捕获
System.out.println("进入异常catch");
e.printStackTrace();
}
}
}

public static void main(String[] args){
Thread myThread = new MyThread();
myThread.start();
}
}

打印输出如下:

1
2
3
4
进入异常catch
java.lang.ThreadDeath
at java.lang.Thread.stop(Thread.java:853)
at stopTest.Test$MyThread.run(Test.java:13)

stop()方法为什么要作废?就是因为如果强制让线程停止有可能使一些清理性的工作得不到完成。

2.6.3 stop与锁

使用stop()释放锁将会给数据造成不一致性的结果。如果出现这样的情况,程序处理的数据就有可能遭到破坏,最终导致程序执行的流程错误,一定要特别注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SynchronizedObject {
private String name = "a";
private String password = "aa";

public synchronized void printString(String name, String password){
try {
this.name = name;
Thread.sleep(100000);
this.password = password;
} catch (InterruptedException e) {
e.printStackTrace();
}
}

// 隐藏get和set
}

---------------------------------------------------

public class MyThread extends Thread {
private SynchronizedObject synchronizedObject;
public MyThread(SynchronizedObject synchronizedObject){
this.synchronizedObject = synchronizedObject;
}

public void run(){
// 调用该方法,想要加锁对象存入name为b替代已有的a,存入password为b替换已有的aa
synchronizedObject.printString("b", "bb");
}
}

public class Run {
public static void main(String args[]) throws InterruptedException {
SynchronizedObject synchronizedObject = new SynchronizedObject();
Thread thread = new MyThread(synchronizedObject);
thread.start();
Thread.sleep(500);
thread.stop();
System.out.println(synchronizedObject.getName() + " " + synchronizedObject.getPassword());
}
}

输出结果:

1
b  aa

由于突如其来的刹车,使用了stop()暴力释放了锁,使得本应该输出 b bb 的,被拦腰斩了一半,只得到b aa了!

由于stop()方法以及在JDK中被标明为“过期/作废”的方法,显然它在功能上具有缺陷,所以不建议在程序张使用stop()方法。


3 小结

以上总结了一些如何去停止线程的方法,最重要的就是使用interrupt打出的那两套组合拳,还讲了interruptedisinterrupted的区别,还讲了一下sleep()方法下进行线程停止会发生什么,最后再浅显的讲解了一下stop方法带来的不好,还是有很多东西需要深入理解的。