内容多有疏漏,有问题欢迎提出
目录
- 线程安全的HashMap:ConcurrentHashMap
- 随机数据结构:ConcurrentSkipListMap
- 高效读取:CopyOnWriteArrayList
- 高效读写的队列:ConcurrentLinkedQueue
- 数据共享通道:BlockingQueue
- 总结
线程安全的HashMap:ConcurrentHashMap
ConcurrentHashMap采用了分段锁机制实现高效的并发访问。ConcurrentHashMap由多个Segment构成,每个Segment都包含一张哈希表。每次操作只将操作数据所属的Segment锁起来,从而避免将整个锁住。
- ConcurrentHashMap内部包含了Segment数组,而每个Segment又继承自ReentrantLock,因此它是一把可重入的锁。
- Segment内部拥有一个HashEntry数组,它就是一张哈希表。HashEntry是单链表的一个节点,HashEntry数组存储单链表的表头节点。
随机数据结构:ConcurrentSkipListMap
- 它是一个有序的Map,相当于TreeMap。
- TreeMap采用红黑树实现排序,而ConcurrentHashMap采用跳表实现有序。
跳表的由来
作用:存储有序序列,并且实现高效的查找与插入删除。
存储有序序列最简单的办法就是使用数组,从而查找可以采用二分搜索,但插入删除需要移动元素较为低效。
因此出现了二叉搜索树,用来解决插入删除移动元素的问题。但二叉搜索树在最坏情况下会退化成一条单链表,搜索的效率降为O(n)。
为了避免二叉搜索树的退化,出现了二叉平衡树,它在每次插入删除节点后都会重新调整树形,使得它仍然保持平衡,从而保证了搜索效率,也保证了插入删除的效率。
此外,根据平衡算法的不同,二叉平衡树又分为:B+树、B-树、红黑树。
但平衡算法过于复杂,因此出现跳表。
跳表介绍
跳表是条有序的单链表,它的每个节点都有多个指向后继节点的引用。
它有多个层次,上层都是下层的子集,从而能跳过不必要的节点,提升搜索速度。
它通过空间来换取时间。
高效读取:CopyOnWriteArrayList
CopyOnWriteArrayList通过在新增元素时, 复制一份新的数组出来, 并在其中写入数据, 之后将原数组引用指向到新数组.
其Add操作是在内部通过ReentrantLock进行锁保护, 防止多线程场景复制多份数组.
而Read操作内部无锁, 直接返回数组引用, 并发下效率高, 因此适用于读多写少的场景。
优点
读操作无需加锁,从而高效。
缺点
- 数据一致性问题
-
- 由于迭代的是容器当前的快照,因此在迭代过程中容器发生的修改并不能实时被当前正在迭代的线程感知。
- 内存占用问题
-
- 由于修改容器都会复制数组,从而当数组超大时修改容器效率很低。
- PS:因此写时复制容器适合存储小容量数据。
高效读写的队列:ConcurrentLinkedQueue
一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。
新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。
ConcurrentLinkedQueue使用CAS非阻塞算法实现使用CAS解决了当前节点与next节点之间的安全链接和对当前节点值的赋值。由于使用CAS没有使用锁,所以获取size的时候有可能进行offer,poll或者remove操作,导致获取的元素个数不精确,所以在并发情况下size函数不是很有用。另外第一次peek或者first时候会把head指向第一个真正的队列元素。
数据共享通道:BlockingQueue
阻塞队列, 主要用于多线程之间共享数据.
当一个线程读取数据时, 如果队列是空的, 则当前线程会进入等待状态。
如果队列满了, 当一个线程尝试写入数据时, 同样会进入等待状态。
适用于生产消费者模型。
因为BlockingQueue在put take等操作有锁, 因此非高性能容器。
总结
这一讲简单讲解了jdk的几个主要并发容器的概念和简单用法,后续会补上详细用法的讲解。
下一章,我们会对锁进行分析,敬请期待。