在Java线程状态转换中,我们知道,线程最终的命运是Terminated,当然,也有永不停止一直干活的线程(除非断电)。线程的停止,正常来说是线程运行到结束,但也有程序出错或是用户关闭程序等原因造成的线程终止。
如何正确停止线程?
正确的停止线程对保护程序数据有重要意义,如果线程运行于一半,执行了一些数据操作,需要后续代码继续运行才能保证数据正确,此时假若你运行了下面的强制关闭线程代码
thread.stoo();
那大概率你的数据将变脏。这个接口虽说可以用,但需要开发人员明确知道使用此接口调用不会造成程序数据异常才能用,JDK已经将此方法设置为过时,就是为了避免开发人员误用。
正确的停止线程姿态应该是通知、协作。更明确的说,就是使用interrupt方法去替代stop方法。下面是一个例子
/**
* @author kangming.ning
* @date 2020/10/12 19:08
*/
public class StopThread implements Runnable {
@Override
public void run() {
int count = 0;
//检查线程是否收到中断信号,收到的话应跳出循环停止工作,让线程正常结束
while (!Thread.currentThread().isInterrupted() && count < 1000) {
System.out.println("count = " + count++);
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new StopThread());
thread.start();
Thread.sleep(20);
thread.interrupt();
}
}
可以看出,上面我使用了interrupt方法去通知线程停止,而线程内部,则在执行代码处先判断是否接收到中断信号,一但接收到中断信号,直接让线程退出执行,线程自然结束。这里需要注意的是下面这句代码
Thread.currentThread().isInterrupted()
这个在线程启动时值为false,当线程调用了interrupt方法,线程实际就是将此值设置为true。interrupt方法并不会去将你的线程给强制终止,而仅仅是通知你。该怎么干,线程自己决定。这个就是通知与协作。
通知与协作的好处
通过上面的例子,可以很容易看出,通知与协作将停止的权利交还给了线程自己,线程接收到停止信号后,可以先完成工作再结束线程。也可以对信号视而不见,这完全由开发人员决定。这样的线程它不会出现不可预料的错误。
InterruptedException 异常处理
上面给出了一个正确停止线程的例子,但实际应用线程可能会复杂,比如在线程内部调用sleep、wait等可以阻塞线程的方法,此时如果调用interrupt方法,中断标志首先会设置为true。当阻塞方法会抛出InterruptedException异常,中断标识设置为false。此时如果程序设计不当,可能会导致线程停不下来,比如下面的例子
public class NeedReInterrupt implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new NeedReInterrupt());
thread.start();
Thread.sleep(200);
thread.interrupt();
}
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 10000) {
try {
System.out.println("count = " + count++);
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
可以看到,在while循环里面,程序捕获了InterruptedException,打印了异常,未有更多操作,此时这个中断异常就被程序吃掉了,这线程将会继续执行循环体直到结束,主线程调用的interrupt并未得到任务效果。当然,如果将while循环放到try里面,程序可正确响应主线程的停止,因为程序不再需要执行判断语句。
像上面的程序出错的原因在于,判断中断信号的位置不当导致。如果确实需要这么设置,可以在catch处再次调用中断,这样一来,循环体就能再次发现中断信号为true,正确结束程序。
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted() && count < 10000) {
try {
System.out.println("count = " + count++);
Thread.sleep(20);
} catch (InterruptedException e) {
//重新设置中断标示 抛出InterruptedException后中断标示会被清除 如果不重新设置的话,整个线程将无法中断
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
如果是在某方法中有相关阻塞方法,可以将方法签名设置抛出InterruptedException,直抛到线程的run方法,run方法catch中断异常进行处理。
死锁线程无法中断
不同于sleep和wait等阻塞情况,死锁导致的阻塞,中断信号是无法被感知的,程序设计尤其要注意这点,下面是一个死锁的例子
public class DeadLockThread extends Thread {
public static void main(String args[]) throws Exception {
final Object lock1 = new Object();
final Object lock2 = new Object();
Thread thread1 = new Thread() {
public void run() {
deathLock(lock1, lock2);
}
};
Thread thread2 = new Thread() {
public void run() {
// 注意,这里在交换了一下位置
deathLock(lock2, lock1);
}
};
System.out.println("Starting thread...");
thread1.start();
thread2.start();
Thread.sleep(3000);
System.out.println("Interrupting thread...");
thread1.interrupt();
thread2.interrupt();
Thread.sleep(3000);
System.out.println("Stopping application...");
}
static void deathLock(Object lock1, Object lock2) {
try {
synchronized (lock1) {
Thread.sleep(10);// 不会在这里死掉
synchronized (lock2) {// 会锁在这里,虽然阻塞了,但不会抛异常
System.out.println(Thread.currentThread());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
System.exit(1);
}
}
}
上面的例子中,两条线程地都永远的阻塞,调用interrupt不会产生任何的作用。