含义: JUCjava.util.concurrent工具包的简称,专门负责处理多线程场景

ArrayList -> CopyOnWriteArrayList

  • 内部持有一个可重入锁ReentrantLock,增删改(add,remove,set)等操作加锁保证线程安全,读(get)不必.
  • 通过读写分离保证了并发效率,写时复制(联想到redis的rdb父子进程)出一个新的数组,写完再把新数组赋值给array.
  • 由于读写分离需要复制新数组,因此适合操作较小的数组,若数组较大则占用内存多.
  • 适合读操作更多或数组元素较小的情景
  • 由于直接复制,不存在扩容操作

HashSet -> CopyOnWriteArraySet

  • CopyOnWriteArraySet内置了一个CopyOnWriteArrayList,所有操作都依赖于CopyOnWriteArrayList,由于set是去重的,因此list里面新增了addIfAbsent等方法来去重.
  • CopyOnWriteArraySet的removeAll等批处理方法实际是调用基础的增删查改等方法,虽然get,set等方法是原子性的,但迭代调用它们却不是,因此调用批处理方法时需要另外加锁才能保证线程安全.
  • CopyOnWriteArraySet的迭代器支持next(),hasNext()等不可变操作,但不支持remove()等可变操作.

TreeSet -> ConcurrentSkipSet

  • 都支持排序.ConcurrentSkipSet依赖于ConcurrentSkipListMap实现,底层是跳表,TreeSet底层是红黑树.
  • 它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整;而对跳表的插入和删除,只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,需要一个全局锁,来保证整个平衡树的线程安全;而对于跳表,则只需要部分锁即可
  • ConcurrentSkipSet的线程安全不是通过加锁实现的,原理有些复杂,可以理解为线程冲突后再重新遍历.日后再细细研究

跳表的本质,是同时维护了多个链表,并且链表是分层的.


HashMap -> ConcurrentHashMap

  • 默认数组大小16,扩容因子0.75.
  • HashMap的底层数据结构是一个hash桶,桶内每个元素又是一个链表,jdk1.8之后,若链表过长会转变为红黑树(查询效率提高至O(log n) )
  • ConcurrentHashMap在HashMap的基础上新增了一个Segment数组.Segment是ReentrantLock的子类,用于实现线程安全.Segment内置了一个HashEntry数组,可以理解成每个segment都是一个hashmap,通过对每个segment分开加锁,避免全局加锁,从而支持了相当于Segment 数组数量的并发量.(分段锁,可以联想到LongAdder对cells数组的CAS操作)
  • HashEntry中的value使用了volatile修饰,不能保证原子性,因此增删改时仍要加锁(tryLock)
  • put操作是两层hash,先是对key进行hash计算定位到相应的segment,然后再hash定位到对应的entry.如果获取锁失败,会自旋获取锁,自旋次数达到上限则改为阻塞锁获取,保证获取成功.
  • 由于entry中的value使用了volatile修饰,保证了可见性,因此get操作不需加锁,,每次获取时都是最新值.

重点讲下1.8下concurrenthashmap的机制:

  • 在jdk1.8中,抛弃了原先的Segment分段锁,转而采用CAS+synchronized,对node数组中单个链表加锁.
  • 构造方法中没有初始化node数组,而是在首次put的时候进行判断是否初始化,属于懒汉式,同时通过一个互斥信号量sizeCtl和CAS来保证初始化或者扩容时的线程安全.
  • put的时候,若node数组未初始化则初始化,若正在扩容则协助扩容,然后若对应的数组位置为空,则通过CAS来写入.若不为空,则对首节点加锁,之后的操作与hashmap一致.
  • get操作遇到扩容时,需要调用正在扩容节点的find方法去nextTable里找,若匹配则返回

TreeMap -> ConcurrentSkipListMap

原理见上.