并发包总览

JUC 并发包结构概览.png

  • Lock 框架。
  • Tools 工具类。
  • Collections 并发集合。
  • Executor:线程池。
  • Atomic:原子类

    类结构总览

    下面是 JUC 类结构总览图:
    类结构总览.png

接口:Condition

Condition 为接口类型,它将 Object 监视器方法 (wait、notify 和 notifyAll) 分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。可以通过 await(),signal() 来休眠/唤醒线程。
等待有三种模式,分别是 ① 可中断等待。② 不可中断等待。③ 限时等待。这些等待也一一在接口定义中体现出来,接口 Condition 共定义有 7 个接口

接口 描述
void await() 使当前线程等待,直到它被唤醒(signal()、signalAll())或被中断。
线程调用这个方法会做:① 主动释放与 Condition 相关的锁。② 交出 CPU 时间片。③ 线程睡眠(主动释放锁)。
有四种方法可以唤醒被阻塞的线程:① 其它线程调用 Condition.signal(),当前线程只好被选中。② 其它线程调用 Condition.signalAll()。③ 其它线程调用了等待线程的 interrupt() 方法,并且等待线程是可响应中断的。④ 虚假唤醒(spurious wakeup)。
在所有情况下,在此方法可以返回当前线程前,必须重新获取 Condition 相关的锁。当线程返回时,它保证持有这把锁。
如果当前线程是被中断的,那么会抛出 InterruptedException 异常(即这个方法是需要处理中断的)。
void awaitUninterruptibly() 使当前线程等待,直接它被唤醒。不会响应中断。
long awaitNanos(nanosTimeout) 使当前线程等待,直接:① 被唤醒 ② 被中断(可响应中断) ③ 超时。
和前面都是一样的,只不过多了超时时间,一旦过了超时时间,就会被唤醒,在方法返回时,必须重新获得锁。
使用方式如下面代码所示。
long await(time, timeunit) 这个方法与 awaitNanos(unit.toNanos(time)) > 0 相等
boolean awaitUntil(Date) 给定一个截止日期
void signal() 唤醒一个处于等待状态的线程
void signalAll() 唤醒所有处于等待状态的线程。每个线程必须在 await 返回前重新获得锁。
  1. //
  2. boolean aMethod(long timeout, TimeUnit unit) {
  3. long nanos = unit.toNanos(timeout);
  4. lock.lock();
  5. try {
  6. while (!conditionBeingWaitedFor()) { // 等待条件
  7. if (nanos <= 0L)
  8. return false;
  9. // 等待时长
  10. nanos = theCondition.awaitNanos(nanos);
  11. }
  12. // ...
  13. } finally {
  14. lock.unlock();
  15. }
  16. }

Condition 接口实现类

Condition接口两种实现.png
从类结构总览图了解到,Condition 共有两个实现类,分别是 AbstractQueuedLongSynchronizerAbstractQueuedSynchronizer 内部类。内部具体细节等到 AQS 时会讲到。

接口:Lock

是 JUC 并发包重量级的接口,Lock 的实现类提供了比 synchronized 关键字更灵活的操作。因为 synchronized 对我们是透明的,JVM 帮且我们做了加锁和释放锁操作,但是 synchronized 只能对单一资源进行锁定,而 Lock 的实现类比 synchronized 丰富得多,比如超时等待、唤醒、获取锁的状态等等。还可以支持对多个资源进行锁定。
一些遍历并发访问数据结构的算法需要 hand-over-hand 或 chainlocking,你首先需要获取节点 A 的锁,然后获取节点 B 的锁,再释放节点 A 的锁,获取节点 C 等等。Lock 的实现类允许在不同范围内获取和释放锁,并允许以任何顺序获取和释放多个锁。但增加灵活性的同时也增加了编程的复杂性。在大多数情况下,就使用以下编程结构:

  1. Lock lock = ...;
  2. lock.lock();
  3. try {
  4. // access the resource protected by this lock
  5. } finally {
  6. // release lock
  7. lock.unlock();
  8. }

详细说明如下表所示:

接口 描述
void lock() 尝试获得锁。如果获取锁失败,线程处于等待状态。
void lockInterruptibly() 尝试获得锁,除非当前线程被中断。即可以响应中断。尝试中断获取锁的操作代价是昂贵的。如果子类实现,应该文档记录下来。与正常方法返回相比,实现可以更倾向于响应中断。
boolean tryLock() 尝试获得锁。如果获取失败,立即返回 false。
boolean tryLock(time, TimeUnit) 在给定的超时时间范围内获得锁。
void unlock() 释放锁。
Condition newCondition() 返回一个新的与 Lock 对象绑定的 Condition 实例。

接口:ReadWriteLock

ReadWriteLock 维护一对关联的锁,一个用于只读操作,一个用于写入操作。读锁属于共享锁,可以同时被多个线程持有,而写锁属于独占锁,某一时刻只能被一个线程所持有。
ReadWriteLock 的所有实现类都必须保证 writeLock() 方法的内存可见性。也就是说,一个成功获得锁的线程将看到之前释放写锁时所做的所有更新。
与使用互斥锁相比,读写锁是否会提高性能取决于读取数据与修改数据的频率、读写操作持续时间以及对数据的急用等等。比如目录结构数据就是使用读写锁的理想候选者。但是,如果更新过于频繁,那么大部分时间都被排他性占用,并且并发性能几乎没有增加。此外,如果读操作太快,读写锁本身可能需要花费更大的开销,特别是大多数 ReadWriteLock 的实现需要 serialize all threads through a samll section of code。最后,只有分析和测试后才能确定读写锁是否符合自身需要。
当读者和写者都在等待锁时,恰好此时写者释放锁,那将决定哪个获得锁呢? 通常来说,写者更容易获得锁,因为写操作耗时短且不频繁。
总的来说,读写锁需要根据场景来确认将锁的所有权交给读线程还是写线程、是否需要保证公平、锁是否可重入、写锁是否可降级为读锁等等。都需要 ReadWriteLock 实现类考虑清楚。
ReadWriteLock 接口有两个实现类,它们之间的区别在于锁是否可重入。
ReadWriteLock实现类.png

抽象类:AbstractOwnableSynchronizer

独占锁的抽象类,线程可以以独占方式拥有同步器。内部结果十分简单,仅仅只有一个变量:exclusiveOwnerThread,它是实现独占锁的关键。如果不为空,意味着锁被该引用指向的线程所持有。

抽象类:AbstractQueuedLongSynchronizer

long 形式维护同步状态的 AbstractQueuedSynchronizer 版本,与 AbstractQueuedSynchronizer 完全镜像,只不过与状态相关的参数和结果都被定义为 long,当需要创建 64 位状态的多级别锁和屏障等同步器时,非常有用。

抽象类:AbstractQueuedSynchronizer

AQS,JUC 并发的核心类。实现依赖于 FIFO 等待队列的阻塞锁和相关同步器(信号量、事件),它是如此丰富的并发包的实现基础。

工具类:LockSupport

用来创建锁和其它同步类的基本线程阻塞原语。可以理解为一个工具类,底层依赖 Unsafe 对线程进行操作。

锁实现类:ReentrantLock

Lock 接口的实现类,它是一个可重入的互斥锁。可以实现 synchronized 相同的语义和行为,但功能更强大,也更灵活。

锁实现类:ReentrantReadWriteLock

ReadWriteLock 的实现类,它包含 Lock 接口的两个实现类:ReadLock(共享锁) 和 WriteLock(独占锁)。此类包含多个内部类:
ReentrantReadWriteLock 相关内部类.png
除了组合 Lock 接口实现类,还组合了 AQS 的实现类,比如 FairSync、NonfairSync 等等。

锁实现:StampedLock

这是 JDK 8 新增的类。它控制锁有三种模式:① 写,② 读,③ 乐观读。一个 StampedLock 状态由版本和模式两个部分组成,锁获取方法返回一个数字作为票据 stamp,它用相应的锁状态表示并控制访问,数字 0 表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。

工具类:CountDownLatch

一个同步辅助类,在完成一组正在其它线程中执行的操作之前,它允许一个或多个线程一直等待。

工具类:CyclicBarrier

一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。在涉及一组固定大小的线程的程序中,这些程序必须不时地互相等待,那么 CyclicBarrier 就非常适合这类场景。因为对象可以复用。

工具类:Phaser

JDK 7 新增的同步辅助类,它可以实现 CyclicBarrier 和 CountDownLatch 类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。

工具类:Semaphore

一个计数器信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行为。通常用于限制可以访问某些资源的线程数目。

工具类:Exchange

用于线程协作的工具类,主要用于两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程可以通过 exchange() 方法交换数据,当一个线程先执行 exchange() 方法后,它会一直等待第二个线程也执行 exchange() 方法,当两个线程都执行方法后,就可以进行数据交换了。

并发集合总览

并发结构类关系图

并发集合类结构关系图.png

对比说明

类名 数据
结构
是否
有界
描述 优势 缺点 协调策略
ArrayBlockingQueue 数组 由数组支撑的有界阻塞队列。按 FIFO 规则对元素排序。队列头部存在时间最长,队列尾部存在时间最短。元素入队被放在队尾。取出操作从队头取出。 操作速度快,不需要产生或销毁额外对象实例。 数组长度有限 速率的调控是通过生产者唤醒消费者、消费者唤醒生产者以实现速率调控
LinkedBlockingQueue 链表 有/无 长度任意,按 FIFO 规则对元素排序。队头最长,队尾最短。 使用双锁以提高吞吐量,伸缩性较好。 影响 GC 使用双锁,尽量让两边各自独立,生产者在队列未满的情况下唤醒生产者。消费者在队列不为空的时候唤醒消费者。
LinkedBlockingDeque 双端队列 长度任意、阻塞的双端队列
ConcurrentLinkedQueue 链表 不允许 null 元素。
ConcurrentLinkedDeque 双端队列
DelayQueue 队列 延时无界阻塞队列
PriorityBlockingQueue 队列 无界优先级阻塞队列
SynchronousQueue 队列 容量0 没有容量的同步队列,通过 CAS 实现并发访问
LinkedTransferQueue 队列 JDK 7 新增,通过 CAS 实现并发访问。可以说是 ConcurrentLinkedQueue、SynchronousQueue 和 LinkedBlockingQueue 的超集
CopyOnWriteArrayList 数组 所有变更操作对底层数组进行复制
CopyOnWriteArraySet Set
ConcurrentSkipListSet Set 跳表
ConcurrentHashMap Map 线程安全的 HashMap
ConcurrentSkipListMap Map

Executors 线程池

Executor 实现类.png

接口:Executor