首先要明确的是Java没有提供任何机制来安全的终止线程。
Java虽然提供了stop方法来终止线程,但是这种方式简单粗暴,很可能造成数据不一致的情况,因此stop方法已经弃用了。
目前较为安全地终止线程方式有两种:
1)使用标志位,让线程run方法在合适的时候结束执行,从而终止线程。
2)使用中断机制,让线程run方法在中断的状态下结束执行,从而终止线程。
ps:这两种方案虽然都可以安全的终止线程,但是使用标志位往往具有局限性,譬如使用阻塞容器的线程,在容器阻塞的情况下,往往没有机会检测到到标志位,从而导致线程无法终止(后面会有实例对比来说明这个问题)。所以,就有下面的结论:
通常,使用中断机制是实现线程终止的最合理方式。
一、使用标志位终止线程
如下实例声明了一个volatile类型的boolean变量作为标志位,表示线程是否终止,当标志位为false时线程才保持运行。
/**
* 通过标志位来停止线程
*/
static class CancelThreadWorker extends Thread {
private volatile boolean cancelled = false;
public CancelThreadWorker(String name) {
super(name);
}
public void run() {
int i = 0;
while (!cancelled) {
try {
System.out.println(i);
Thread.sleep(2000);
i++;
} catch (InterruptedException e) {
ExceptionUtil.printException(e);
}
}
}
public void cancel() {
cancelled = true;
}
}
ps:为了后期测试方便,写个简单的工具类MyThreadUtil对基础打印操作做了封装,如下:
static class MyThreadUtil {
public static void printException(Exception e) {
System.out.println(Thread.currentThread().getName() + " throws an Exception:" + e);
}
public static void printState(Thread thread) {
System.out.println(thread.getName() + " current state is:" + thread.getState());
}
}
main方法测试代码如下:
CancelThreadWorker cancelThreadWorker = new CancelThreadWorker("cancelThreadWorker");
cancelThreadWorker.start();
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
ExceptionUtil.printException(e);
}
cancelThreadWorker.cancel();
while (cancelThreadWorker.isAlive()) {} //阻塞一小段时间,等待线程终止
System.out.println(cancelThreadWorker.getState());
控制台输出信息如下:
0
1
cancelThreadWorker current state is:TERMINATED
可以看出使用标志位确实让线程终止了。
二、使用中断机制
中断是一种协作机制,调用interrupt方法并不代表线程立刻就停止了,而是告诉线程你应该停止了。
Java中设计一个良好的api通常要考虑对中断做出及时响应,方便调用者知道被中断的线程接收到了中断信息,从而可以提前结束线程。例如Thread.sleep,Object.wait,Thread.join,阻塞容器的take和put,这类方法之所以被设计成抛出中断异常InterruptedException,就是为了对中断做出及时响应,告诉调用者,我已经收到中断信号了!值得注意的是,这类库方法在抛出InterruptedException的时候会清除线程的中断状态。
如下的实例演示了如何通过中断机制来终止线程,通过判断当前线程中断状态是否为false,仅当中断状态为false时run方法才执行。
当外部线程调用了该线程的interrupt方法时,sleep方法会对中断做出响应,抛出InterruptedException,继而重置中断状态为false。由于run方法限制了异常只能捕获,所以我们必须对异常及时处理,这里处理方案有两种,1)重新调用当前线程的interrupt方法让中断状态恢复为true。2)直接使用break退出循环即可
static class InterruptThreadWorker extends Thread {
public InterruptThreadWorker(String name) {
super(name);
}
public void run() {
int i = 0;
while (!Thread.currentThread().isInterrupted()) {
try {
System.out.println(i);
Thread.sleep(2000);
i++;
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
Thread.currentThread().interrupt(); //由于中断异常会清除中断标记,所以此处一定要重新调用中断方法。当然也可以用break来代替
// break; //使用break代替上面的interrupt也可以
}
}
}
}
main方法测试代码如下:
InterruptThreadWorker interruptThreadWorker = new InterruptThreadWorker("interruptThreadWorker");
interruptThreadWorker.start();
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
interruptThreadWorker.interrupt(); //传递中断信号,告诉interruptThreadWorker,你该终止了哈
while (interruptThreadWorker.isAlive()) {} //阻塞一小段时间,等待线程终止
MyThreadUtil.printState(interruptThreadWorker);
控制台输出如下:
0
1
interruptThreadWorker throws an Exception:java.lang.InterruptedException: sleep interrupted
interruptThreadWorker current state is:TERMINATED
可以看出在这个案例和上一个案例的演示结果是一样的。
三、使用标志位方案终止线程的局限性
前面提到过标志位的终止方案对于阻塞容器来说可能会失效,如下是一个具体的案例,源于<Java并发编程实战>这本书。
/**
* 素数生产者线程
*/
static class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled = false;
public PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
BigInteger p = BigInteger.ONE.ONE;
while (!cancelled) {
try {
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
}
}
public void cancel() {
cancelled = true;
}
}
素数生产者线程PrimeProducer维护了一个阻塞容器BlockingQueue,run方法使用while循环判断标志位cancelled是否为false,如果满足则向容器中添加素数。
main方法的测试代码如下,我们设置阻塞队列的长度为9,主线程睡眠20ms之后,设置标志位为false。
BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9);
PrimeProducer brokenPrimeProducer = new PrimeProducer(primes);
brokenPrimeProducer.start();
try {
Thread.sleep(20);
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
brokenPrimeProducer.cancel();
System.out.println(brokenPrimeProducer.queue.size());
while (brokenPrimeProducer.isAlive()) {} //阻塞一小段时间,等待线程终止
MyThreadUtil.printState(brokenPrimeProducer);
控制台打印如下:
9
细心观察,发现控制台其实一直处于运行状态,并没有打印这个线程的状态,也就是说线程并没有被终止,而是一直处于阻塞状态。
这是因为:线程开启的20ms之后,阻塞队列已经被填满,而put作为一个阻塞方法(类似的take),如果队列满则会持续到阻塞到队列有可用空间为止。本例中只有生成者线程而没有消费者线程,导致put方法持续阻塞,所以无法检测到while循环标志位的状态,故线程无法终止。
如果我们将main方法中的阻塞队列扩大,线程会被正常终止的。只需修改下面一行:
BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<BigInteger>(9999);
或者:你也可以保持队列容量不变,将主线程的睡眠时间缩小:
Thread.sleep(2);
采用第一种实验方案,控制台打印如下:
56
Thread-0 current state is:TERMINATED
可以看出阻塞队列在添加完第56个元素之后,发现标志位为true,不再添加元素,线程终止。证明前面的分析完全成立。
显然实际项目开发中,我们不可能为了终止线程来刻意改变阻塞队列大小,这是一种投机行为,包含了运气成分,它可能会影响项目质量。所以对于包含阻塞队列的线程来说,不提倡使用标志位的策略终止线程。
本案例中对于素数生产者线程来说,使用中断是最好的策略,将PrimeProducer改造成以下形式:
/**
* 素数生产者线程
*/
static class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
public PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
BigInteger p = BigInteger.ONE.ONE;
while (!isInterrupted()) {
try {
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
cancel();
}
}
}
public void cancel() {
this.interrupt();
}
}
依旧设置阻塞队列大小为9,延时20ms,main方法运行效果如下,可以看出控制台打印之后正常结束,线程不再阻塞。
9
Thread-0 throws an Exception:java.lang.InterruptedException
Thread-0 current state is:TERMINATED
四、stop方法不安全的体现
stop方法不适合用来终止线程,是因为它是强行结束线程,如果对象是一种不连贯的状态,即使方法是同步的,也会立刻释放已持有的锁,从而造成数据不一致。
如下的案例演示这个问题:
static class UnstableObject {
private String first = "ja";
private String second = "va";
public synchronized void setValues(String first, String second) throws Exception{
this.first = first;
Thread.sleep(10000);
this.second = second;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getSecond() {
return second;
}
public void setSecond(String second) {
this.second = second;
}
}
UntableObject这个类维护了两个属性first和second,并提供了一个同步方法setValues对两个属性分别赋值,然而赋值过程并不是连贯的,存在一个睡眠10s的过程,如果一个线程调用setValues并在10s内进行被stop,则只会对第一个属性成功赋值,第二个属性仍然保持默认。
我们测试代码如下:
UnstableObject unstableObject = new UnstableObject();
Thread thread = new Thread(()->{
try {
unstableObject.setValues("p", "hp");
} catch (Exception e) {
e.printStackTrace();
}
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
MyThreadUtil.printException(e);
}
thread.stop();
MyThreadUtil.printState(thread);
System.out.println(unstableObject.getFirst() + unstableObject.getSecond());
控制台输出如下:
Thread-0 current state is:TERMINATED
pva
可以看出线程虽然终止了,但我们渴望输出php,它却输出了pva,这就是stop不安全的一个体现。