Java 如何停止一个线程

在Java中如何才能正在启动一个线程Thread,实际上使用的是线程Thread的start()方法。但是如何停止一个正在运行的线程呢?线程Thread类提供了stop()方法,可是实际开发中几乎没有使用过线程Thread自己提供的stop()方法,因为stop()方法从JDK1.2开始就已经Deprecated,下面是JDK中对stop()方法的描述。

该方法具有固有的不安全性。用 Thread.stop 来终止线程将释放它已经锁定的所有监视器(作为沿堆栈向上传播的未检查 ThreadDeath 异常的一个自然后果)。如果以前受这些监视器保护的任何对象都处于一种不一致的状态,则损坏的对象将对其他线程可见,这有可能导致任意的行为。stop的许多使用都应由只修改某些变量以指示目标线程应该停止运行的代码来取代。目标线程应定期检查该变量,并且如果该变量指示它要停止运行,则从其运行方法依次返回。如果目标线程等待很长时间(例如基于一个条件变量),则应使用 interrupt 方法来中断该等待。有关更多信息,请参阅为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?。

假设在同一时刻,银行账户A去汇款到账户B,账户B也在查询账户余额,如果ThreadA线程拥有了监视器,这些监视器负责保护某些临界资源,这里临界资源就是汇款金额。然而在汇款过程中,ThreadA线程调用了threadA.stop()方法。结果导致监视器被释放,那么临界资源汇款金额很可能出现不一致性,比方账户A减少了100,但是账户B却没有增加100,而线程ThreadB执行查询账户B余额发现确实没有增加,那么这100元跑哪去了?

中断停止线程

《并发编程实践》一书中介绍,Java没有提供任何机制来安全地终止线程。但它提供了中断(interruption),这是一种协作机制,能够使一个线程终止另一个线程当前的工作。正如上面JDK中对stop方法的描述,应该使用interrupt方法来中断线程。

java.lang.Thread类中提供了如下几个方法处理中断状态:

方法名称 方法描述
public static boolean interrupted 测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

public boolean isInterrupted()

测试线程是否已经中断。线程的中断状态不受该方法的影响。
public void interrupt()

中断线程。

下面是一个使用interrupt相关方法的Demo。

public class MainTest {

	public static void main(String[] args) {
		Task task = new Task();
		task.start();
		task.interrupt();
		System.out.println(task.isInterrupted());
		System.out.println(Thread.interrupted());

	}

	private static class Task extends Thread {
		@Override
		public void run() {
			for (int i = 0; i < 100; i++) {
				System.out.println(i + ":" + Thread.interrupted());
			}
		}
	}

}
//true
//0:true
//false
//1:false
//2:false
//3:false
//4:false
//...

通过上面介绍,我们知道stop()方法是过时的不再建议使用的,但是通过示例发现,Thread类中interrupt()方法并没有停止一个正在运行的线程,这是为什么呢?JDK中对interrupt()方法的描述也看不出所以然来,如下是JDK中的描述。

如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的 join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。

如果该线程在可中断的通道上的 I/O 操作中受阻,则该通道将被关闭,该线程的中断状态将被设置并且该线程将收到一个 ClosedByInterruptException。

如果该线程在一个 Selector 中受阻,则该线程的中断状态将被设置,它将立即从选择操作返回,并可能带有一个非零值,就好像调用了选择器的 wakeup 方法一样。

《并发编程实践》是这样描述interrupt()方法的,调用interrupt()方法并不意味着立刻停止目标线程正在进行的工作,而只是传递了请求中断的消息。也即是说,它不会真正的中断一个正在运行的线程,而只是发送了一下中断请求(可以简单的理解为就是设置了一个中断标记位),然后由线程在下一个合适的时刻中断自己。

每一个线程都有一个boolean类型的中断状态位。当中断线程时,该线程的中断标记位将会设置为true。interrupt()方法能够中断目标线程并设置中断标记位,而isInterrupted()方法能返回目标线程的中断状态。静态方法interrupted()会清除线程的中断状态,并返回它原来的值,这也是清除中断状态的唯一方法。

接下来再看一下示例的运行结果就容易理解了,当我们调用task.interrupt()方法时,实际上设置了中断标记位true,所以接下来调用task.isInterrupted()查看线程的中断状态,直接返回true,在子线程中首次调用静态方法Thread.interrupted()将会清除中断标记,所以第二次调用时直接返回false,跟上面表格中介绍一致。

接着上面的讨论,JDK描述线程Thread中的stop()方法是Deprecated,建议使用interrupt()方法替代,但是interrupt()方法并不会停止一个正在运行的线程,在对interrupt()方法的描述中也可以知道,它仅仅是设置了一个中断标记。我们可以在上面例子中每次输出时使用isInterrupted()方法判断一下线程是否已经中断,这样如果已经设置了中断标记直接跳出循环即可。

public class MainTest {

	public static void main(String[] args) {
		Task task = new Task();
		task.start();
		task.interrupt();
	}

	private static class Task extends Thread {
		@Override
		public void run() {
			for (int i = 0; i < 100; i++) {
				if(Thread.currentThread().isInterrupted()){
					System.out.println("interrupted");
					break;
				}else{
					System.out.println(i + ":" + Thread.interrupted());
				}
				
			}
		}
	}
}

通过这种实现方式,线程调用interrupt()方法后确实立刻就会退出循环了,可是问题到了这里还没有结束。JDK中描述了有关阻塞库方法,例如Thread类中的sleep()、join()以及Object类中wait()方法,它们都会检查何时中断,并且在中断时提前返回,而且在相应中断时执行如下操作:清除中断状态,抛出InterruptedException。下面是JDK对sleep()方法的描述。

/**
 * Causes the currently executing thread to sleep (temporarily cease
 * execution) for the specified number of milliseconds, subject to
 * the precision and accuracy of system timers and schedulers. The thread
 * does not lose ownership of any monitors.
 *
 * @param  millis
 *         the length of time to sleep in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          interrupted status of the current thread is
 *          cleared when this exception is thrown.
 */
public static native void sleep(long millis) throws InterruptedException;

在for循环中将线程执行以下睡眠,每格100ms输出一个数值。

public class MainTest {

	public static void main(String[] args) {
		Task task = new Task();
		task.start();
		task.interrupt();
	}

	private static class Task extends Thread {
		@Override
		public void run() {
			for (int i = 0; i < 100; i++) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				if(Thread.currentThread().isInterrupted()){
					System.out.println("interrupted");
					break;
				}else{
					System.out.println(i + ":" + Thread.interrupted());
				}
			}
		}
	}
}
//java.lang.InterruptedException: sleep interrupted
//	at java.lang.Thread.sleep(Native Method)
//	at com.sunny.demo.MainTest$Task.run(MainTest.java:18)
//0:false
//1:false
//2:false
//3:false
//...

运行Demo发现线程又不能够停止了,因为sleep()方法会响应中断,然后清除中断标记,抛出异常,注意下面再对中断标记进行判断就会出现Thread.currentThread().isInterrupted()为false,这样造成线程还会继续执行下去。由于阻塞库方法会中断线程,抛出异常,所以下面的代码可以说是模板方法:

public void run() { try { // ① 调用阻塞方法 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // ② 恢复被中断的状态 } }

当阻塞方法响应中断后再次调用线程的interrupt()方法恢复中断,这样问题似乎得到了解决。

在实际开发过程中,很多业务是不可以直接将异常捕获直接就这样简单处理掉的,一般情况下有两种策略可用于处理InterruptedException。

  • 恢复中断状态,这样可以让调用栈中的上层代码能够对其进行处理;
  • 传递异常(可能在执行某个特定的操作之后),从而使你的方法称为可中断的阻塞方法。

使用interrupt()确实可以中断并停止线程,可是实际开发中一般都会重新定义一个方法进行线程的停止操作,在多线程开发中,却很难知道写的代码将在哪个线程中运行,这也是在平常开发过程中很难直接看到一个停止或者取消的方法中有直接使用interrupt()方法。再者,调用interrupt()方法时,需要处理InterruptedException异常恢复中断状态,可是不是所有的阻塞方法都能响应中断,例如一般IO操作阻并不会抛出InterruptedException。

那么如何才能保证线程真正可靠的停止呢?

在线程同步的时候我们有一个叫"二次惰性检测"(double check),能在提高效率的基础上又确保线程真正中同步控制中。

那么我把线程正确退出的方法称为"双重安全退出",即不以isInterrupted()为循环条件,而以一个标记作为循环条件。

共享标志停止线程

在文章开始部分为何不赞成使用 Thread.stop、Thread.suspend 和 Thread.resume?。 介绍了使用一个volatile修饰的变量作为线程停止的一个标记,并在在停止的方法中调用interrupt()方法。

private volatile Thread blinker;

public void stop() {
	Thread moribund = waiter;
	waiter = null;
	moribund.interrupt();
}

public void run() {
	Thread thisThread = Thread.currentThread();
	while (blinker == thisThread) {
		try {
			thisThread.sleep(interval);
		} catch (InterruptedException e) {
		}
		repaint();
	}
}

在这个示例中线程的停止标记是一个volatile修饰的线程,而实际开发中一般都是volatile修饰的Boolean类型或者整形的变量。JDK中自带cancel()方法的类FutureTask中使用的就是一个volatile修饰的变量state,在判断线程是否停止时,而且在cancel方法中将线程停止时使用了interrup()方法,代码如下:

public boolean isCancelled() {
	return state >= CANCELLED;
}
public boolean cancel(boolean mayInterruptIfRunning) {
	if (!(state == NEW &&
		  UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
			  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
		return false;
	try {    // in case call to interrupt throws exception
		if (mayInterruptIfRunning) {
			try {
				Thread t = runner;
				if (t != null)
					t.interrupt();
			} finally { // final state
				UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
			}
		}
	} finally {
		finishCompletion();
	}
	return true;
}

在线程池ThreadPoolExecutor类中,用于判断线程池是否已经关闭的isShutdown方法中也是对共享变量进行的判断,ctl是一个AtomicInteger原子整形变量。而且在线程池类中shutdown方法中同样使用了interrupt()方法,shutdown()方法的源码这里就不再贴出来了。

public boolean isShutdown() {
	return ! isRunning(ctl.get());
}

private static boolean isRunning(int c) {
	return c < SHUTDOWN;
}

小结

通过上面简单的介绍可以知道如何要停止一个线程,最好的方式就是中断+条件变量双重安全退出策略。由于Java语言没有提供任何机制来安全的退出线程,它只提供了中断机制,这是一种协作机制,可以让一个线程停止另一个线程的工作。所以在Java语言开发中想要使一个线程安全、快速、可靠地停止下来,并不是一件容易的事,在多线程开发中更多的建议是使用JDK中提供的线程池类,这样不仅使用方便,而且不会因为频繁创建和销毁线程对系统产生过大的开销。

参考资料

Why are Thread.stop, Thread.suspend and Thread.resume Deprecated ?

Thread的中断机制(interrupt)

理解java线程的中断(interrupt)

详细分析Java中断机制

如何停止一个正在运行的java线程

评论

您确定要删除吗?删除之后不可恢复