《实战 Java 高并发程序设计》笔记——第3章 JDK 并发包(一)

声明:

本博客是本人在学习《实战 Java 高并发程序设计》后整理的笔记,旨在方便复习和回顾,并非用作商业用途。

本博客已标明出处,如有侵权请告知,马上删除。

为了更好地支持并发程序,JDK 内部提供了大量实用的 API 和框架。在本章中,将主要介绍这些 JDK 内部的功能,其主要分为三大部分:

  • 首先,将介绍有关同步控制的工具,之前介绍的 synchronized 关键字就是一种同步控制手段,在这里,我们将看到更加丰富多彩的多线程控制方法。
  • 其次,将详细介绍 JDK 中对线程池的支持,使用线程池,将能很大程度上提高线程调度的性能。
  • 第三,我将向大家介绍 JDK 的一些并发容器,这些容器专为并行访问所设计,绝对是高效、安全、稳定的实用工具。

3.1 多线程的团队协作:同步控制

同步控制是并发程序必不可少的重要手段。之前介绍的 synchronized 关键字就是一种最简单的控制方法。它决定了一个线程是否可以访问临界区资源。同时,Object.wait() 和 Object.notify() 方法起到了线程等待和通知的作用。这些工具对于实现复杂的多线程协作起到了重要的作用。在本节中,我们首先将介绍 synchronized、Object.wait() 和 Object.notify() 方法的替代品(或者说是增强版)——重入锁

3.1.1 synchronized 的功能扩展:重入锁

重入锁可以完全替代 synchronized 关键字。在 JDK 5.0 的早期版本中,重入锁的性能远远好于 synchronized,但从 JDK 6.0 开始,JDK 在 synchronized 上做了大量的优化,使得两者的性能差距并不大。

重入锁使用 java.util.concurrent.locks.ReentrantLock 类来实现。下面是一段最简单的重入锁使用案例:

在这里插入图片描述

上述代码第 7~12 行,使用重入锁保护临界区资源 i,确保多线程对 i 操作的安全性。从这段代码可以看到,与 synchronized 相比,重入锁有着显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁。也正因为这样,重入锁对逻辑控制的灵活性要远远好于 synchronized。但值得注意的是,在退出临界区时,必须记得释放锁(代码第 11 行),否则,其他线程就没有机会再访问临界区了。

有些同学可能会对重入锁的名字感到奇怪。锁就叫锁呗,为什么要加上 “重入” 两个字呢?从类的命名上看,Re-Entrant-Lock 翻译成重入锁也是非常贴切的。之所以这么叫,那是因为这种锁是可以反复进入的。当然,这里的反复仅仅局限于一个线程。上述代码的第 7~12 行,可以写成下面的形式:

在这里插入图片描述

在这种情况下,一个线程连续两次获得同一把锁。这是允许的!如果不允许这么操作,那么同一个线程在第 2 次获得锁时,将会和自己产生死锁。程序就会 “卡死” 在第 2 次申请锁的过程中。但需要注意的是,如果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。如果释放锁的次数多,那么会得到一个 java.lang.IllegalMonitorStateException 异常,反之,如果释放锁的次数少了,那么相当于线程还持有这个锁,因此,其他线程也无法进入临界区。

除了使用上的灵活性外,重入锁还提供了一些高级功能。比如,重入锁可以提供中断处理的能力。

中断响应

对于 synchronized 来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,则提供另外一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。有些时候,这么做是非常有必要的。比如,如果你和朋友约好一起去打球,如果你等了半小时,朋友还没有到,突然接到一个电话,说由于突发情况,不能如约了。那么你一定就扫兴地打道回府了。中断正式提供了一套类似的机制。如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无须再等待,可以停止工作了。这种情况对于处理死锁是有一定帮助的

下面的代码产生了一个死锁,但得益于锁中断,我们可以很轻易地解决这个死锁。

在这里插入图片描述

线程 t1 和 t2 启动后,t1 先占用 lock1,再占用 lock2;t2 先占用 lock2,再请求 lock1。因此,很容易形成 t1 和 t2 之间的相互等待。在这里,对锁的请求,统一使用 lockInterruptibly() 方法。这是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断

在代码第 47 行,主线程 main 处于休眠,此时,这两个线程处于死锁的状态,在代码第 49 行,由于 t2 线程被中断,故 t2 会放弃对 lock1 的申请,同时释放已获得 lock2。这个操作导致 t1 线程可以顺利得到 lock2 而继续执行下去。

执行上述代码,将输出:

在这里插入图片描述

可以看到,中断后,两个线程双双退出。但真正完成工作的只有 t1。而 t2 线程则放弃其任务直接退出,释放资源。

锁申请等待限时

除了等待外部通知之外,要避免死锁还有另外一种方法,那就是限时等待。依然以约朋友打球为例,如果朋友迟迟不来,又无法联系到他。那么,在等待 1~2 个小时后,我想大部分人都会扫兴离去。对线程来说也是这样。通常,我们无法判断为什么一个线程迟迟拿不到锁。也许是因为死锁了,也许是因为产生了饥饿。但如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。我们可以使用 tryLock() 方法进行一次限时的等待

下面这段代码展示了限时等待锁的使用。

在这里插入图片描述

在这里,tryLock() 方法接收两个参数,一个表示等待时长,另外一个表示计时单位。这里的单位设置为秒,时长为 5,表示线程在这个锁请求中,最多等待 5 秒。如果超过 5 秒还没有得到锁,就会返回 false。如果成功获得锁,则返回 true。

在本例中,由于占用锁的线程会持有锁长达 6 秒,故另一个线程无法在 5 秒的等待时间内获得锁,因此,请求锁会失败。

ReentrantLock.tryLock() 方法也可以不带参数直接运行。在这种情况下,当前线程会尝试获得锁,如果锁并未被其他线程占用,则申请锁会成功,并立即返回 true。如果锁被其他线程占用,则当前线程不会进行等待,而是立即返回 false。这种模式不会引起线程等待,因此也不会产生死锁。下面演示了这种使用方式:

在这里插入图片描述

上述代码中,采用了非常容易死锁的加锁顺序。也就是先让 t1 获得 lock1,再让 t2 获得 lock2,接着做反向请求,让 t1 申请 lock2,t2 申请 lock1。在一般情况下,这会导致 t1 和 t2 相互等待,从而引起死锁。

但是使用 tryLock() 后,这种情况就大大改善了。由于线程不会傻傻地等待,而是不停地尝试,因此,只要执行足够长的时间,线程总是会得到所有需要的资源,从而正常执行(这里以线程同时获得 lock1 和 lock2 两把锁,作为其可以正常执行的条件)。在同时获得 lock1 和 lock2 后,线程就打印出标志着任务完成的信息 “My Job done”。

执行上述代码,等待一会儿(由于线程中包含休眠 500 毫秒的代码)。最终你还是可以欣喜地看到程序执行完毕,并产生如下输出,表示两个线程双双正常执行。

在这里插入图片描述

公平锁

在大多数情况下,锁的申请都是非公平的。也就是说,线程 1 首先请求了锁 A,接着线程 2 也请求了锁 A。那么当锁 A 可用时,是线程 1 可以获得锁还是线程 2 可以获得锁呢?这是不一定的。系统只是会从这个锁的等待队列中随机挑选一个。因此不能保证其公平性。这就好比买票不排队,大家都乱哄哄得围在售票窗口前,售票员忙得焦头烂额,也顾不及谁先谁后,随便找个人出票就完事了。而公平的锁,则不是这样,它会按照时间的先后顺序,保证先到者先得,后到者后得。公平锁的一大特点是:它不会产生饥饿现象。只要你排队,最终还是可以等到资源的。如果我们使用 synchronized 关键字进行锁控制,那么产生的锁就是非公平的。而重入锁允许我们对其公平性进行设置。它有一个如下的构造函数:

在这里插入图片描述

当参数 fair 为 true 时,表示锁是公平的。公平锁看起来很优美,但是要实现公平锁必然要求系统维护一个有序队列,因此公平锁的实现成本比较高,性能相对也非常低下,因此,默认情况下,锁是非公平的。如果没有特别的需求,也不需要使用公平锁。公平锁和非公平锁在线程调度表现上也是非常不一样的。下面的代码可以很好地突出公平锁的特点:

在这里插入图片描述

上述代码第 2 行,指定锁是公平的。接着,由两个线程 t1 和 t2 分别请求这把锁,并且在得到锁后,进行一个控制台的输出,表示自己得到了锁。在公平锁的情况下,得到输出通常如下所示:

在这里插入图片描述

由于代码会产生大量输出,这里只截取部分进行说明。在这个输出中,很明显可以看到,两个线程基本上是交替获得锁的,几乎不会发生同一个线程连续多次获得锁的可能,从而公平性也得到了保证。如果不使用公平锁,那么情况会完全不一样,下面是使用非公平锁时的部分输出:

在这里插入图片描述

可以看到,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的,但是无公平性可言。

对上面 ReentrantLock 的几个重要方法整理如下

  • lock() :获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly() :获得锁,但优先响应中断。
  • tryLock() :尝试获得锁,如果成功,返回 true,失败返回 false。该方法不等待,立即返回。
  • tryLock(long time,TimeUnit unit) :在给定时间内尝试获得锁。
  • unlock() :释放锁。

就重入锁的实现来看,它主要集中在 Java 层面。在重入锁的实现中,主要包含三个要素

  • 第一,是原子状态。原子状态使用 CAS 操作(在第 4 章进行详细讨论)来存储当前锁的状态,判断锁是否已经被别的线程持有。
  • 第二,是等待队列。所有没有请求到锁的线程,会进入等待队列进行等待。待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
  • 第三,是阻塞原语 park() 和 unpark() ,用来挂起和恢复线程。没有得到锁的线程将会被挂起。有关 park() 和 unpark() 的详细介绍,可以参考 3.1.7 线程阻塞工具类:LockSupport。

3.1.2 重入锁的好搭档:Condition 条件

如果大家理解了 Object.wait() 和 Object.notify() 方法的话,那么就能很容易地理解 Condition 对象了。它和 wait() 和 notify() 方法的作用是大致相同的。但是 wait() 和 notify() 方法是和 synchronized 关键字合作使用的,而 Condtion 是与重入锁相关联的。通过 Lock 接口(重入锁就实现了这一接口)的 Condition newCondition() 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象,我们就可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行

Condition 接口提供的基本方法如下:

在这里插入图片描述

以上方法的含义如下:

  • await() 方法会使当前线程等待,同时释放当前锁,当其他线程中使用 signal() 或者 signalAll() 方法时,线程会重新获得锁并继续执行。或者当线程被中断时,也能跳出等待。这和 Object.wait() 方法很相似。
  • awaitUninterruptibly() 方法与 await() 方法基本相同,但是它并不会在等待过程中响应中断。
  • singal() 方法用于唤醒一个在等待中的线程。相对的 singalAll() 方法会唤醒所有在等待中的线程。这和 Obejct.notify() 方法很类似。

下面的代码简单地演示了 Condition 的功能:

在这里插入图片描述

代码第 3 行,通过 lock 生成一个与之绑定的 Condition 对象。代码第 8 行,要求线程在 Condition 对象上进行等待。代码第 23 行,由主线程 main 发出通知,告知等待在 Condition 上的线程可以继续执行了。

和 Object.wait() 和 notify() 方法一样,当线程使用 Condition.await() 时,要求线程持有相关的重入锁,在 Condition.await() 调用后,这个线程会释放这把锁。同理,在 Condition.signal() 方法调用时,也要求线程先获得相关的锁。在 signal() 方法调用后,系统会从当前 Condition 对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在 signal() 方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。比如,在本例中,第 24 行代码就释放了重入锁,如果省略第 24 行,那么,虽然已经唤醒了线程 t1,但是由于它无法重新获得锁,因而也就无法真正的继续执行。

在 JDK 内部,重入锁和 Condition 对象被广泛地使用,以 ArrayBlockingQueue 为例(可以参阅 “3.3 JDK 并发容器” 一节),它的 put() 方法实现如下:

在这里插入图片描述

同理,对应 take() 方法实现如下:

在这里插入图片描述

3.1.3 允许多个线程同时访问:信号量(Semaphore)

信号量为多线程协作提供了更为强大的控制方法。广义上说,信号量是对锁的扩展。无论是内部锁 synchronized 还是重入锁 ReentrantLock,一次都只允许一个线程访问一个资源,而信号量却可以指定多个线程,同时访问某一个资源。信号量主要提供了以下构造函数:

在这里插入图片描述

在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。当每个线程每次只申请一个许可时,这就相当于指定了同时有多少个线程可以访问某一个资源。信号量的主要逻辑方法有:

在这里插入图片描述

acquire() 方法尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或者当前线程被中断。acquireUninterruptibly() 方法和 acquire() 方法类似,但是不响应中断。tryAcquire() 尝试获得一个许可,如果成功返回 true,失败则返回 false,它不会进行等待,立即返回。release() 用于在线程访问资源结束后,释放一个许可,以使其他等待许可的线程可以进行资源访问。

在 JDK 的官方 Javadoc 中,就有一个有关信号量使用的简单实例,有兴趣的读者可以自行翻阅,这里我给出一个更加傻瓜化的例子:

在这里插入图片描述

上述代码中,第 7~9 行为临界区管理代码,程序会限制执行这段代码的线程数。这里在第 2 行,申明了一个包含 5 个许可的信号量。这就意味着同时可以有 5 个线程进入代码段第 7~9 行。申请信号量使用 acquire() 操作,在离开时,务必使用 release() 释放信号量(代码第 10 行)。这就和释放锁是一个道理。如果不幸发生了信号量的泄露(申请了但没有释放),那么可以进入临界区的线程数量就会越来越少,直到所有的线程均不可访问。在本例中,同时开启 20 个线程。观察这段程序的输出,你就会发现系统以 5 个线程一组为单位,依次输出带有线程 ID 的提示文本。

3.1.4 ReadWriteLock 读写锁

ReadWriteLock 是 JDK5 中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。用锁分离的机制来提升性能非常容易理解,比如线程 A1、A2、A3 进行写操作,B1、B2、B3 进行读操作,如果使用重入锁或者内部锁,则理论上说所有读之间、读与写之间、写和写之间都是串行操作。当 B1 进行读取时,B2、B3 则需要等待锁。由于读操作并不对数据的完整性造成破坏,这种等待显然是不合理。因此,读写锁就有了发挥功能的余地。

在这种情况下,读写锁允许多个线程同时读,使得 B1、B2、B3 之间真正并行。但是,考虑到数据完整性,写写操作和读写操作间依然是需要相互等待和持有锁的。总的来说,读写锁的访问约束如表 3.1 所示。

在这里插入图片描述

  • 读-读不互斥:读读之间不阻塞。
  • 读-写互斥:读阻塞写,写也会阻塞读。
  • 写-写互斥:写写阻塞。

如果在系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能。这里我给出一个稍微夸张点的案例,来说明读写锁对性能的帮助。

在这里插入图片描述

上述代码中,第 11 行和第 21 行分别模拟了一个非常耗时的操作,让线程耗时 1 秒钟。它们分别对应读耗时和写耗时。代码第 34 和 45 行,分别是读线程和写线程。在这里,第 34 行使用读锁,第 45 行使用写锁。第 53~55 行开启 18 个读线程,第 57~59 行,开启两个写线程。由于这里使用了读写分离,因此,读线程完全并行,而写会阻塞读,因此,实际上这段代码运行大约 2 秒多就能结束(写线程之间是实际串行的)。而如果使用第 35 行代替第 34 行,使用第 46 行代替第 45 行执行上述代码,即使用普通的重入锁代替读写锁。那么所有的读和写线程之间都必须相互等待,因此整个程序的执行时间将长达 20 余秒。

3.1.5 倒计时器:CountDownLatch

CountDownLatch 是一个非常实用的多线程控制工具类。“Count Down” 在英文中意为倒计数,Latch 为门闩的意思。如果翻译成为倒计数门闩,我想大家都会觉得不知所云吧!因此,这里简单地称之为倒计数器。在这里,门闩的含义是:把门锁起来,不让里面的线程跑出来。因此,这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行

对于倒计时器,一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。只有等所有的检查完毕后,引擎才能点火。这种场景就非常适合使用 CountDownLatch。它可以使得点火线程等待所有检查线程全部完工后,再执行。

CountDownLatch 的构造函数接收一个整数作为参数,即当前这个计数器的计数个数

在这里插入图片描述

下面这个简单的示例,演示了 CountDownLatch 的使用。

在这里插入图片描述

上述代码第 2 行,生成一个 CountDownLatch 实例。计数数量为 10。这表示需要有 10 个线程完成任务,等待在 CountDownLatch 上的线程才能继续执行。代码第 10 行,使用了 CountDownLatch.countdown() 方法,也就是通知 CountDownLatch,一个线程已经完成了任务,倒计时器可以减 1 啦。第 21 行,使用 CountDownLatch.await() 方法,要求主线程等待所有 10 个检查任务全部完成。待 10 个任务全部完成后,主线程才能继续执行。

上述案例的执行逻辑可以用图 3.1 简单表示。

在这里插入图片描述

主线程在 CountDownLatch 上等待,当所有检查任务全部完成后,主线程方能继续执行。

3.1.6 循环栅栏:CyclicBarrier

CyclicBarrier 是另外一种多线程并发控制实用工具。和 CountDownLatch 非常类似,它也可以实现线程间的计数等待,但它的功能比 CountDownLatch 更加复杂且强大

CyclicBarrier 可以理解为循环栅栏。栅栏就是一种障碍物,比如,通常在私人宅邸的周围就可以围上一圈栅栏,阻止闲杂人等入内。这里当然就是用来阻止线程继续执行,要求线程在栅栏处等待。前面 Cyclic 意为循环,也就是说这个计数器可以反复使用。比如,假设我们将计数器设置为 10,那么凑齐第一批 10 个线程后,计数器就会归零,然后接着凑齐下一批 10 个线程,这就是循环栅栏内在的含义。

CyclicBarrier 的使用场景也很丰富。比如,司令下达命令,要求 10 个士兵一起去完成一项任务。这时,就会要求 10 个士兵先集合报道,接着,一起雄赳赳气昂昂地去执行任务。当 10 个士兵把自己手头的任务都执行完成了,那么司令才能对外宣布,任务完成!

比 CountDownLatch 略微强大一些,CyclicBarrier 可以接收一个参数作为 barrierAction。所谓 barrierAction 就是当计数器一次计数完成后,系统会执行的动作。如下构造函数,其中,parties 表示计数总数,也就是参与的线程总数。

在这里插入图片描述

下面的示例使用 CyclicBarrier 演示了上述司令命令士兵完成任务的场景。

在这里插入图片描述

上述代码第 57 行,创建了 CyclicBarrier 实例,并将计数器设置为 10,并要求在计数器达到指标时,执行第 43 行的 run() 方法。每一个士兵线程会执行第 11 行定义的 run() 方法。在第 14 行,每一个士兵线程都会等待,直到所有的士兵都集合完毕。集合完毕后,意味着 CyclicBarrier 的一次计数完成,当再一次调用 CyclicBarrier.await() 时,会进行下一次计数。第 15 行,模拟了士兵的任务。当一个士兵任务执行完毕后,他就会要求 CyclicBarrier 开始下一次计数,这次计数主要目的是监控是否所有的士兵都已经完成了任务。一旦任务全部完成,第 35 行定义的 BarrierRun 就会被调用,打印相关信息。

上述代码的执行输出如下:

在这里插入图片描述

整个工作过程的图示如图 3.2 所示。

在这里插入图片描述

CyclicBarrier.await() 方法可能会抛出两个异常

  • 一个是 InterruptedException,也就是在等待过程中,线程被中断,应该说这是一个非常通用的异常。大部分迫使线程等待的方法都可能会抛出这个异常,使得线程在等待时依然可以响应外部紧急事件。
  • 另外一个异常则是 CyclicBarrier 特有的 BrokenBarrierException。一旦遇到这个异常,则表示当前的 CyclicBarrier 已经破损了,可能系统已经没有办法等待所有线程到齐了。如果继续等待,可能就是徒劳无功的,因此,还是就地散货,打道回府吧!上述代码第 18~22 行处理了这 2 种异常。

如果我们在上述代码的第 63 行后,插入以下代码,使得第 5 个士兵线程产生中断:

在这里插入图片描述

如果这样做,我们很可能就会得到 1 个 InterruptedException 和 9 个 BrokenBarrierException。这个 InterruptedException 就是被中断线程抛出的。而其他 9 个 BrokenBarrierException,则是等待在当前 CyclicBarrier 上的线程抛出的。这个异常可以避免其他 9 个线程进行永久的、无谓的等待(因为其中一个线程已经被中断,等待是没有结果的)。

3.1.7 线程阻塞工具类:LockSupport

LockSupport 是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和 Thread.suspend() 相比,它弥补了由于 resume() 在前发生,导致线程无法继续执行的情况。和 Object.wait() 相比,它不需要先获得某个对象的锁,也不会抛出 InterruptedException 异常。

LockSupport 的静态方法 park() 可以阻塞当前线程,类似的还有 parkNanos() 、parkUntil() 等方法。它们实现了一个限时的等待。

大家应该还记得,我们在第 2 章中提到的那个有关 suspend() 永久卡死线程的例子吧!现在,我们可以用 LockSupport 重写这个程序:

在这里插入图片描述

注意,这里只是将原来的 suspend() 和 resume() 方法用 park() 和 unpark() 方法做了替换。当然,我们依然无法保证 unpark() 方法发生在 park() 方法之后。但是执行这段代码,你会发现,它自始至终都可以正常的结束,不会因为 park() 方法而导致线程永久性的挂起。

这是因为 LockSupport 类使用类似信号量的机制。它为每一个线程准备了一个许可,如果许可可用,那么 park() 函数会立即返回,并且消费这个许可(也就是将许可变为不可用),如果许可不可用,就会阻塞。而 unpark() 则使得一个许可变为可用(但是和信号量不同的是,许可不能累加,你不可能拥有超过一个许可,它永远只有一个)。

这个特点使得:即使 unpark() 操作发生在 park() 之前,它也可以使下一次的 park() 操作立即返回。这也就是上述代码可顺利结束的主要原因

同时,处于 park() 挂起状态的线程不会像 suspend() 那样还给出一个令人费解的 Runnable 的状态。它会非常明确地给出一个 WAITING 状态,甚至还会标注是 park() 引起的

在这里插入图片描述

这使得分析问题时格外方便。此外,如果你使用 park(Object) 函数,还可以为当前线程设置一个阻塞对象。这个阻塞对象会出现在线程 Dump 中。这样在分析问题时,就更加方便了。

比如,如果我们将上述代码第 14 行的 park() 改为:

在这里插入图片描述

那么在线程 Dump 时,你可能会看到如下信息:

在这里插入图片描述

注意,在堆栈中,我们甚至还看到了当前线程等待的对象,这里就是 ChangeObjectThread 实例。

除了有定时阻塞的功能外,LockSupport.park() 还能支持中断影响。但是和其他接收中断的函数很不一样,LockSupport.park() 不会抛出 InterruptedException 异常。它只是会默默的返回,但是我们可以从 Thread.interrupted() 等方法获得中断标记。

在这里插入图片描述

注意上述代码在第 27 行,中断了处于 park() 状态的 t1。之后,t1 可以马上响应这个中断,并且返回。之后在外面等待的 t2 才可以进入临界区,并最终由 LockSupport.unpark(t2) 操作使其运行结束。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值