高并发场景中的集合总结

2023-01-10 15:19:58 浏览数 (2)

还有哪些高并发场景中的常用集合没有被提及

由于篇幅所限,本书不能一一概括JUC中的所有集合,下面对Java中的其他原生集合进行补充说明。

• SynchronousQueue队列:这是一个内部只能存储一个数据对象的阻塞队列,很明显它也是一个有界队列。该队列最显著的工作特点是,一个调用者在向该队列中放入一个数据对象后,会进入阻塞状态,直到另一个调用者将队列中的这个数据对象取出;如果一个调用者需要从该队列中取出一个数据对象,但该队列中恰好没有数据对象,那么该调用者也会进入阻塞状态,直到另一个调用者向该队列中放入一个数据对象为止。总而言之,就是向队列中放入数据对象的生产者线程和从队列中取出数据对象的消费者线程要成对出现。

• ConcurrentLinkedQueue队列:和LinkedBlockingQueue队列相比,这也是一种内部基于链表的,可以在有高并发场景中使用的容量无界的、具有先进先出工作特点的队列。但它不是一种阻塞队列,其内部主要使用基于CAS的乐观锁进行实现。

• LinkedBlockingDeque队列:该队列覆盖了LinkedBlockingQueue队列提供的功能,并且在此基础上增加了双端队列的工作特点,甚至其内部的实现原理也借鉴了LinkedBlockingQueue队列内部的实现原理,如主要使用基于AQS的悲观锁进行实现。两种队列在设计细节上还是有所区别的。例如,LinkedBlockingDeque双端队列内部只有一把锁,该锁可以同时对读/写操作进行互斥控制,并且通过两个独立的Condition控制器对读/写操作线程进行同步控制。

典型集合对应关系对比

Java提供的典型原生集合(List集合、Map集合、Set集合)一般可以在JUC中找到对应的支持高并发场景的集合(但也并不是必然能找到),其对应关系如表10-1所示。

高并发场景中的集合可借鉴的设计思想

根据本书对JUC中的集合(包括Queue集合、Deque集合、Map集合、List集合、Set集合等)进行的介绍可知,Java提供的工作在高并发场景中的原生集合的性能并不是在任何使用场景中都是最好的。这一点在各种常用的Queue/Deque集合中表现得尤其明显。一些第三方组织或公司通常需要根据自己的性能要求,基于Java提供的保证线程安全的基本要素,重新设计所需的集合。此外,这些第三方组织或公司会公布一些已经成熟、稳定的集合供程序员使用。

不过JUC提供的集合在大部分高并发场景中已足够稳定,并且适合运行在大部分高并发场景中。其反映出来的设计思路具有通用性,读者可以在掌握了原子性、可见性和有序性的保障要领、解决并发冲突的战术技巧后,改良现有的集合,或者重新设计新的适合工作在更高并发场景中的集合。

使用JUC提供的基本要素保证线程安全性

要保证线程安全性,至少需要保证三方面的要求:对内存可见性的要求,对执行有序性的要求、对操作原子性的要求。在保证了线程安全性在这三方面的要求后,即可根据工作场景提高工作性能,主要的设计思路包括寻找平均时间复杂度更低的操作方式,寻找减少线程间不必要协作(互斥与同步)的方式,寻找具有更优线程操作平衡性的方式,等等。

下面讲解JUC提供的集合如何解决线程安全性问题。实际上JUC提供了两种解决线程安全性问题的方法,这个在本书中多次提及。一种方法是基于悲观锁思想的AQS技术(注意AQS底层也是基于CAS技术进行实现的),另一种方法是基于乐观锁思想的CAS技术。这两种技术在JUC提供的各种集合中都有体现。

如果使用AQS技术保证线程安全性,那么集合内部无须分别针对有序性、原子性、可见性进行单独处理,因为AQS技术本身已经将资源的操作权单一化,所以基于AQS技术工作的高并发集合的关键共享属性不会单独使用volatile修饰符进行修饰,也不会独立调用任何CAS执行工具,其核心逻辑的处 理 过 程 相 对 简 单 。 这 种 集 合 有 ArrayBlockingQueue 队 列 、LinkedBlockingQueue 队 列 、 PriorityBlockingQueue 队 列 、 DelayQueue 队列、CopyOnWriteArrayList集合等。AQS技术为上层的程序员屏蔽了更多线程安全性问题的处理细节,可以让程序员专注于对所需业务的处理工作,而不用关注如何保证线程安全性的特定要素。对于实现乐观锁思想的CAS技术,需要程序员自行分析和解决编码过程中保证线程安全性的三大问题,这主要是因为CAS技术只能保证操作的原子性,无法保证内存可见性和执行的有序性。而程序员能够观察到的效果是,那些直接基于CAS技术工作的集合,其主要的共享属性都需要自行使用volatile修饰符进行修饰,并且需要随时考虑处理过程中无序操作的边缘性问题。这种集合有TransferQueue队列、ConcurrentHashMap集合(一部分场景)、ConcurrentSkipListMap集合等。

注意并发性能非常好的ConcurrentSkipListMap集合的实现,虽然本书没有介绍该集合。ConcurrentSkipListMap集合主要使用基于CAS技术的乐观锁实现,通过观察该集合在JDK 9 中的实现可以发现,该集合的关键属性并没有使用volatile修饰符进行修饰。那么ConcurrentSkipListMap集合如何保证线程安全性呢?

前面介绍过,volatile修饰符的底层技术是内存屏障,内存屏障可以保证数据对象的可见性和有序性。为了提高性能,在JDK 9 中,Java直接在VarHandle变量句柄工具类中封装了内存屏障(组合)指令,程序员可以直接使用特定的内存屏障(组合)指令,用于保证只增加符合执行要求的最小内存屏障。ConcurrentSkipListMap集合进行数据对象添加操作的示例代码如下。

通过复合手段保证多场景中的性能平衡性

在保证线程安全性的前提下,JUC中的集合是如何解决特定场景中的处理性能问题的?可以这样说,乐观锁设计思想和悲观锁设计思想在实现某个集合的多种功能时,通常不是单独存在的,通常以一种设计思想为主,以另一种设计思想为辅进行实现。

典 型 的 例 子 可 以 参 考 PriorityBlockingQueue 队 列 的 扩 容 问 题 。

PriorityBlockingQueue队列主要基于AQS技术实现高并发场景,当操作线程要 对 PriorityBlockingQueue 队 列 进 行 读 / 写 操 作 时 , 首 先 需 要 获 得PriorityBlockingQueue队列的独占操作权。在对该集合内部的堆进行扩容操作时,会出现两个问题,首先堆的扩容操作花费的时间较长,如果所有线程全部阻塞等待,则会使所有线程的阻塞时间明显变长,导致不必要的性能损耗;其次堆的扩容操作不需要移动数据对象,因此那些因为读操作而阻塞的操作线程没有必要在扩容阶段继续阻塞下去。

那么PriorityBlockingQueue队列在扩容阶段放弃了使用基于AQS技术的悲观锁处理方法,释放掉了当前进行扩容操作的线程拥有的独占操作权,转而使用保证原子性的CAS技术完成扩容操作。使用CAS技术完成扩容操作还有一个好处,就是可以顺带解决重复扩容的问题:由于释放掉了当前进行扩容操作的线程拥有的独占操作权,因此可能造成多个写操作线程同时进入扩容过程,但没有关系,因为只有正确操作了allocationSpinLock属性的线程,才能真正完成扩容操作。

这种复合手段在ConcurrentHashMap集合中也有应用,后者在工作中主要应用基于CAS技术的乐观锁对数组进行处理。例如,对数组进行数据对象添加操作,对数组进行扩容操作,对数组进行数据对象迁移操作,等等。在数组对加锁维度进行细化后,采用基于Object Monitor模式的悲观锁进行对象独占操作权的控制,可以使加锁操作基本保持在JVM对锁自旋或锁偏向的控制级别,而无须将锁升级为重量级锁(为什么使用Object Monitor模式而非AQS技术,在介绍ConcurrentHashMap集合时已经进行了说明,此处不再赘述)。简单地说,一种单一的锁实现方式,并不能解决高并发场景中集合工作的全方位问题,在保证线程安全的情况下,只有针对不同的工作场景采用不同的工作模式,才能对集合的工作性能进行平衡。

0 人点赞