JAVA线程停止的最佳实践
1. 错误的停止方式:两种常见错误
2. 正确的停止方式:如何使用interrupt
开始容易,结束难。形容线程再合适不过了。关于线程停止这块,东西很多,众说纷纭,今天就来梳理下,线程停止的最佳实践。
错误的停止方式
首先简要了解下常见的错误停止方式,以及错在哪里。
这里的错误并不是绝对的错误。可能某些情况下,它依旧能够符合程序运行的期望,但并不能很好的处理所有情况。这里把健壮性不够的方法都归到错误的方法。
1. stop()
假设一个场景,开启一个子线程来模拟银行排队取钱,每个人都取1W元。流程是,柜员先把你银行卡的1W元划掉,然后分10次每次给你1000元。
private static Runnable runnable = () -> {
try {
while (true) {
System.out.println("从银行卡扣掉此人1W元");
for (int j = 1; j < 11; j++) {
System.out.println("给了" + 1000 * j);
//等待印钞机印钱
Thread.sleep(10);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(runnable);
t.start();
Thread.sleep(1000);
t.stop();
}
执行上面的代码最终可能输出
...
给了10000
从银行卡扣掉此人1W元
给了1000
给了2000
线程停止
即可能会出现从卡里扣了1W,却只给人2000的情况。我们期望整个扣钱和取钱的操作是原子性的。但是由于stop()
的直接停止,导致我们没有机会去回滚处理异常情况。
错误之处:stop()
粗暴停止,不给你善后处理的机会。
Ps. 有人说
stop()
时不会释放锁,容易造成死锁。其实是错误的,stop()
会释放所有的monitor,而另一个方法suspend()
则不会。
2. volatile boolean标志位
这种方式也是经常使用的方式,上面直接用stop()
不行。那么我们来改用volatile标志位来控制。
private volatile static boolean running = true;
private static Runnable runnable = () -> {
try {
while (running) {
System.out.println("从银行卡扣掉此人1W元");
for (int j = 1; j < 11; j++) {
System.out.println("给了" + 1000 * j);
//等待印钞机印钱
Thread.sleep(10);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程停止");
};
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(runnable);
t.start();
Thread.sleep(1000);
running = false;
}
通过这种方式保证了每次while循环的原子性,打印结果也完全正常,完美!
但是我们现在来改动下需求。现在柜员找钱没那么容易了,只sleep(10)
找不到钱了,而是需要从印钞机那里取。因此我们定义一个印钞机,作为钱的生产者,而柜员就是钱的消费者。于是代码改动如下,变成了典型的生产者消费者模型。并且我们在生产者和消费者线程停止时打印日志。
Ps. 如果对BlockingQueue不熟悉建议先了解一下。
//控制柜员取钱的标志位
private volatile static boolean consuming = true;
//控制印钞机印钞票的标志位
private volatile static boolean producing = true;
//存钱的保险柜
private static ArrayBlockingQueue<Integer> moneyQueue = new ArrayBlockingQueue<>(10);
private static Runnable consumerRunnable = () -> {
try {
while (consuming) {
//每次1000,取够1W下一位
for (int j = 1; j < 11; j++) {
moneyQueue.take();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者线程停止");
};
private static Runnable producerRunnable = () -> {
try {
while (producing) {
moneyQueue.put(1000);
//印钞机每10ms生产1000块钱
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产者线程停止");
};
public static void main(String[] args) throws InterruptedException {
Thread consumerThread = new Thread(consumerRunnable);
Thread producerThread = new Thread(producerRunnable);
consumerThread.start();
producerThread.start();
Thread.sleep(2000);
consuming = false;
producing = false;
}
运行上面的代码,我们期望设置consuming
和producing
为false时,两个线程都停止,但是,运行后控制台输出
生产者线程停止
可见消费者的线程并没有停止。
原因就是**当生产者停止后,由于moneyQueue
一直为空,会导致消费者永远阻塞在moneyQueue.take()
这里,一直在等待生产者印钞票,根本执行不到while (consuming)
来结束线程。**所以这时消费者线程永远不会停止。
错误之处:循环内部如果有阻塞方法,可能导致线程永远不会停止。