线程的中断和等待/唤醒操作是多线程中非常重要的操作,我们将在本篇介绍如果中断一个线程,以及线程中断的特性。同时也会介绍线程基本的等待/唤醒操作。希望通过本篇的介绍,大家能够理解这两个重要操作,为后面的并发编程学习做好基础。
在上一篇博文中,我们介绍了让线程停止执行的方法,但是让线程直接停止太暴力了,完全不符合我们中国人含蓄的特征,所以 JDK 提供了另外一种方式来阻止线程的执行,这就是线程的中断。线程的中断相对于线程的停止来讲更加温和,他不会强制线程立即退出,而是通知线程你应该停下了,但是至于线程接到这个通知后停不停下来,那就只能由线程本身决定了。
对于线程的中断,通常有以下三个方法:
interrupt() //这个方法用于向线程发送一个中断信号(也可以理解为中断线程)
isInterrupted() //判断线程是否收到中断信号(也可以理解为线程是否被中断)
isInterrupted() //判断线程是否收到中断信号,同时清除这个信号(即如果当前收到了中断信号,下次在判断就是没有收到信号)
我们来看一个线程中断的例子,在这个例子中当线程收到中断信号后不进行中断,继续执行,示例代码如下:
public class InterruptedWithoutReaction { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new TestThread()); t.start(); Thread.sleep(1000); t.interrupt(); } } class TestThread implements Runnable { @Override public void run() { while (true) { System.out.println("====="); try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println("我就喜欢看你想中断我又不能强制中断我的样子..."); } } } }
我们在启动线程 1s 后向线程发送中断信号,通知线程中断,但是让线程在收到中断信号后不进行中断,继续执行。我们来看运行结果截图:
我可以看到,线程收到中断信号后没有进行中断,继续往下执行。这是因为上例中的 Thread.sleep() 方法会及时的收到线程中断信号并通知给线程,另外,当 Thread.sleep() 收到信号后会清除这个信号,使线程下次不再认为自己被中断了。这就印证了我们开篇所说的线程的中断是由线程本身控制,而不能被强制中断。
那么我们应该怎么处理收到的线程中断信号呢?其实很简单,我们只需要像处理线程停止一样加入一段逻辑判断即可让线程在接到中断信号后顺利退出线程任务。我们来看示例代码:
public class BizInterrupted { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new Runnable() { @Override public void run() { while (true) { if (Thread.currentThread().isInterrupted()) { System.err.println("被中断了"); break; } // 做一个耗时的字符串拼接操作,模拟真实业务的耗时 String ss = "a" + "b" + 100 + "d" + "e" + true + "g" + 30.55566666 + "i" + 'j' + "k" + "l" + "m" + "n"; String s = (System.currentTimeMillis() / 1000) + ":Thread-" + Thread.currentThread().getName() + ",Id=" + Thread.currentThread().getId(); System.out.println(s + ss); } } }); t.start(); Thread.sleep(100); t.interrupt(); } }
我们在本次正式执行线程任务之前先判断当前线程是否已经收到了线程中断信号,如果收到了信号则退出线程任务,否则正常执行。为了模拟真实的业务操作的时间消耗,我们在示例代码中做了一个无意义的字符串拼接操作,增加线程任务的耗时。我们来看运行结果截图:
我们看到,线程接收到中断信号后顺利的退出了线程任务,结束了整个程序。我们上面还提到,如果是 Thread.sleep() 接收到了中断信号,他会通知给线程并清除这个信号,那么对于这种情况线程能够顺利退出吗?我们来看这种情况的示例代码:
public class ThreadInterrupted { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread() { @Override public void run() { while (true) { if (Thread.currentThread().isInterrupted()) { System.out.println("线程已收到中断信号"); break; } try { System.out.println("线程任务正常执行...."); Thread.sleep(3000); } catch (InterruptedException e) { System.err.println("线程在睡眠中被中断===="); } } } }; t1.start(); Thread.sleep(2000); t1.interrupt(); } }
我们让主线程在 t1 线程启动 2s 后向 t1 线程发送中断信号,线程任务会让 t1 线程睡眠 3s,也就是说, t1 线程会在睡眠中接收到线程中断信号,即 Thread.sleep() 方法会在阻塞线程的情况下收到中断信号并立即通知给线程,线程收到中断信号,会进入异常处理。我们在线程任务一开始也进行了线程是否被中断的逻辑判断,以保证收到了中断信号的线程不会继续执行,我们来看运行结果截图:
很不幸,我们希望的 t1 线程接到中断信号后退出线程任务的情况并没有如期而至。原因很简单,我们一开始就提到过,Thread.sleep() 在接收到中断信号后虽然会通知给线程,但是这之后他会立即清除这个中断信号,这就让我们在线程任务一开始进行的判断变得没有意义,线程将一直执行下去。要处理这个情况也非常简单,我们只需要在异常处理逻辑中再获得一个中断信号即可,即在 catch 块中添加这一句代码:
Thread.currentThread().interrupt();
这样我们再运行这个示例就会看到线程收到中断信号后正常退出线程任务,整个程序顺利退出了。
我们在这里不使用教科书上的定义对线程的等待和唤醒进行讲解,我们举个生活中的例子来更浅显易懂的说明这两者。我们假设有一个乞丐 A 和一个白领 B,现在 A 和 B 相遇,A 向 B 进行乞讨。情景流程如下:
A 遇到 B,向 B 进行乞讨
B 盯着 A ,等待 A 掏钱(如果 B 不给 A 钱,A 就一直盯着 B,直到 B 给 A 钱)
B 拿了 10 块钱给 A (B 给了 A 钱,告诉 A 不要再盯着我了)
B 离开 A
A 成功获得钱,乞讨成功
在上面的情景流程中,流程 2 就是线程的等待,流程 3 就是线程的唤醒。我们将这个情景转换成一个具体的代码示例:
/** * 线程等待和唤醒示例 * @author leon.gan */ public class BasicNW { public static final Object o = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new AThread(o)); Thread t2 = new Thread(new BThread(o)); t1.start(); Thread.sleep(1000); t2.start(); } } class AThread implements Runnable { private Object o; public AThread(Object o) { this.o = o; } @Override public void run() { synchronized (o) { System.out.println("A 开始乞讨..."); try { System.out.println("A 等待 B 掏钱..."); o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("A 乞讨成功..."); } } } class BThread implements Runnable { private Object o; public BThread(Object o) { this.o = o; } @Override public void run() { synchronized (o) { System.out.println("B 拿钱给 A..."); o.notify(); try { System.out.println("B 给了钱胸口痛了2秒..."); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("B 离开了 A..."); } } }
我们首先启动 A 线程,1s 后再启动 B 线程。A 线程会在线程任务中进行等待,进入等待状态的 A 线程会释放锁,接着 B 线程进入线程任务执行线程唤醒(即 notify() 方法)。notify() 方法会唤醒任意一个基于同一把锁的等待线程,这里的“任意”是指没有任何规律,完全是随机的选择一个线程。这里又有一个重点,线程的等待和唤醒必须是基于同一把锁的,在上例中就是一个 java.lang.Object 的对象 o 。换句话说,o.notify() 这个操作只能唤醒任意一个执行过 o.wait() 操作的线程。另外,我们也可以使用 notifyAll() 方法来唤醒所有等待的线程。
最后我们再来比较抽象的描述一下线程的等待和唤醒。当一个线程在执行到某一个任务步骤时,他可能需要一些资源才能完成这个步骤,而这时这些需要的资源并没有准备好,所以这个线程就应该进入等待状态,等待资源准备好。而资源的准备是另外一个线程在完成的工作,所以进入等待状态后这个线程应该释放锁,让准备资源的线程可以获得锁进入临界区执行线程任务准备资源。当资源准备完毕后,准备资源的线程会唤醒这个等待资源的线程继续执行,最终任务顺利完成。注意,当线程被唤醒后会从上次的等待点继续往下执行,而不是重新从头执行。
本篇我们介绍了多线程中比较重要的两种操作,线程的中断和线程的等待/唤醒。我们需要记住,线程最终是否中断是由线程自己决定的,我们是无法通过发送中断信号强制中断线程的。另外,线程的等待和唤醒操作必须是基于同一把锁的,notify() 会唤醒的任意一个线程也是没有规律,完全随机的。