0%

停止线程的最佳实践

要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java没有提供任何机制来安全地终止线程。但它提供了中断( Interruption),这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

在Java中,最好的停止线程的方式是使用中断interrupt,但是这仅仅是会通知到被终止的线程“你该停止运行了”,被终止的线程自身拥有决定权(决定是否、以及何时停止),这依赖于请求停止方和被停止方都遵守一种约定好的编码规范。

通常情况下的停止线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

/**
* run方法内没有sleep或者wait方法时,停止线程
* @author gelong
* @date 2020/6/3 22:16
*/
public class RightWayStopThreadWithoutSleep {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= 100000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
}
});
thread.start();
Thread.sleep(10);
thread.interrupt();
System.out.println("任务结束了");
}
}

stopthread1.jpg

当在main线程里面调用thread.interrupt()给线程中断的信号时,isInterrupted()方法就会返回true,while条件不满足就会退出循环,此时线程就会退出了。

当线程中断遇到遇到等待状态时

当线程在等待状态时收到中断信号时,会抛出InterruptedException异常。

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

/**
* @author gelong
* @date 2020/6/3 23:24
*/
public class RightWayStopThreadWithSleep {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
Thread.sleep(500);
thread.interrupt();
System.out.println("任务结束了");
}
}

stopthread2.jpg

抛出InterruptedException异常后还会清除interrupt标志位(从true变为false),isInterrupted()方法会
返回false,线程仍然会继续运行。

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

/**
* 抛出InterruptedException会清除中断标记
* @author gelong
* @date 2020/6/8 0:05
*/
public class CantInterrupt {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(2000);
thread.interrupt();
System.out.println("任务结束了");
}
}

stopthread3.jpg

最佳实践

处理中断的最好方法是优先选择在方法上抛出异常。用throws InterruptedException 标记你的方法,不采用try 语句块捕获异常,以便于该异常可以传递到顶层,让run()方法可以捕获这一异常,由run()决定是否停止线程,例如:

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

/**
* 在方法签名中抛出异常
* @author gelong
* @date 2020/6/9 21:31
*/
public class RightWayStopThreadInProd implements Runnable {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}

@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
try {
throwInMethod();
} catch (InterruptedException e) {
System.out.println("停止线程");
e.printStackTrace();
}
}
}

private void throwInMethod() throws InterruptedException {
Thread.sleep(1000);
}
}

由于run方法内无法抛出checked Exception(只能用try catch),顶层方法必须处理该异常,避免了漏掉或者被吞掉的情况,增强了代码的健壮性。

如果不想或无法传递InterruptedException(例如用run方法的时候,就不让该方法throws InterruptedException),那么应该选择在catch 子句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行依然能够检查到刚才发生了中断。

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

/**
* 恢复设置中断状态
* @author gelong
* @date 2020/6/9 21:31
*/
public class RightWayStopThreadInProd2 implements Runnable {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}

@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
reInterrupt();
}
}

private void reInterrupt() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}

还有一种方法是使用volatile来停止线程,volatile能够保证可见性,但是这种停止的方法不够全面。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
* @author gelong
* @date 2020/6/9 22:01
*/
public class WrongWayVolatileCantStop {

public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Producer producer = new Producer(queue);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = new Consumer(queue);
while (consumer.needMore()) {
System.out.println(consumer.queue.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
producer.cancel = true;
}
}

class Producer implements Runnable {

BlockingQueue<Integer> queue;
volatile boolean cancel = false;

public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}

@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !cancel) {
if (num % 100 == 0) {
queue.put(num);
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}

class Consumer {
BlockingQueue<Integer> queue;

public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}

public boolean needMore() {
Random random = new Random();
return random.nextDouble() <= 0.95;
}
}

stopthread4.jpg

当生产者生产太快而消费者消费太慢时,线程可能会阻塞在queue.put()方法导致线程无法感应到cancel的变化而无法退出线程。所以正确的停止线程做法还是使用中断,代码如下:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
* @author gelong
* @date 2020/6/9 22:01
*/
public class WrongWayVolatileFixed {

public static void main(String[] args) throws InterruptedException {
WrongWayVolatileFixed body = new WrongWayVolatileFixed();
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
Producer producer = body.new Producer(queue);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = body.new Consumer(queue);
while (consumer.needMore()) {
System.out.println(consumer.queue.take() + "被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
producerThread.interrupt();
}
class Producer implements Runnable {

BlockingQueue<Integer> queue;

public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}

@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
queue.put(num);
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者结束运行");
}
}
}

class Consumer {
BlockingQueue<Integer> queue;

public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}

public boolean needMore() {
Random random = new Random();
return random.nextDouble() <= 0.95;
}
}
}

stopthread5.jpg

总结

除了以上的Thead.sleep()方法和queue.put()方法能够响应中断之外,Object.wait()/Thread.join()等等方法都能够响应中断。