并发包总览
接口: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 返回前重新获得锁。 |
//
boolean aMethod(long timeout, TimeUnit unit) {
long nanos = unit.toNanos(timeout);
lock.lock();
try {
while (!conditionBeingWaitedFor()) { // 等待条件
if (nanos <= 0L)
return false;
// 等待时长
nanos = theCondition.awaitNanos(nanos);
}
// ...
} finally {
lock.unlock();
}
}
Condition 接口实现类
从类结构总览图了解到,Condition 共有两个实现类,分别是 AbstractQueuedLongSynchronizer
和 AbstractQueuedSynchronizer
内部类。内部具体细节等到 AQS 时会讲到。
接口:Lock
是 JUC 并发包重量级的接口,Lock 的实现类提供了比 synchronized 关键字更灵活的操作。因为 synchronized 对我们是透明的,JVM 帮且我们做了加锁和释放锁操作,但是 synchronized 只能对单一资源进行锁定,而 Lock 的实现类比 synchronized 丰富得多,比如超时等待、唤醒、获取锁的状态等等。还可以支持对多个资源进行锁定。
一些遍历并发访问数据结构的算法需要 hand-over-hand 或 chainlocking,你首先需要获取节点 A 的锁,然后获取节点 B 的锁,再释放节点 A 的锁,获取节点 C 等等。Lock 的实现类允许在不同范围内获取和释放锁,并允许以任何顺序获取和释放多个锁。但增加灵活性的同时也增加了编程的复杂性。在大多数情况下,就使用以下编程结构:
Lock lock = ...;
lock.lock();
try {
// access the resource protected by this lock
} finally {
// release lock
lock.unlock();
}
详细说明如下表所示:
接口 | 描述 |
---|---|
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 接口有两个实现类,它们之间的区别在于锁是否可重入。
抽象类:AbstractOwnableSynchronizer
独占锁的抽象类,线程可以以独占方式拥有同步器。内部结果十分简单,仅仅只有一个变量:exclusiveOwnerThread
,它是实现独占锁的关键。如果不为空,意味着锁被该引用指向的线程所持有。
抽象类:AbstractQueuedLongSynchronizer
以 long
形式维护同步状态的 AbstractQueuedSynchronizer
版本,与 AbstractQueuedSynchronizer
完全镜像,只不过与状态相关的参数和结果都被定义为 long
,当需要创建 64 位状态的多级别锁和屏障等同步器时,非常有用。
抽象类:AbstractQueuedSynchronizer
AQS
,JUC 并发的核心类。实现依赖于 FIFO 等待队列的阻塞锁和相关同步器(信号量、事件),它是如此丰富的并发包的实现基础。
工具类:LockSupport
用来创建锁和其它同步类的基本线程阻塞原语。可以理解为一个工具类,底层依赖 Unsafe
对线程进行操作。
锁实现类:ReentrantLock
Lock 接口的实现类,它是一个可重入的互斥锁。可以实现 synchronized 相同的语义和行为,但功能更强大,也更灵活。
锁实现类:ReentrantReadWriteLock
ReadWriteLock 的实现类,它包含 Lock 接口的两个实现类:ReadLock(共享锁) 和 WriteLock(独占锁)。此类包含多个内部类:
除了组合 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() 方法,当两个线程都执行方法后,就可以进行数据交换了。
并发集合总览
并发结构类关系图
对比说明
类名 | 数据 结构 |
是否 有界 |
描述 | 优势 | 缺点 | 协调策略 |
---|---|---|---|---|---|---|
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 | 无 |