学习记录 3/25
ConcurrentHashMap
- jdk1.7
- 数据结构是Segment+数组+链表
- 使用
Segment分段锁机制控制并发,默认并发级别是16(一定是2的幂),默认容量是16(即默认每个Segment中的Hash数组的长度是16/16=1==》2,一定是2的幂),默认负载因子是0.75
- 扩容是
Segment独立扩容,每个Segment中都是独立的HashMap,扩容的触发条件是单个Segment中的元素个数达到其扩容阈值。
ConcurrentHashMap的总容量只用在初始化的时候计算每个Segment中的容量。
- 插入会首先hash计算选择Segment再计算桶的位置,获取锁之后先遍历是否有相同的,有则覆盖,没有则头插法
- key和value都不允许为
NULL
- size()方法统计元素数量,需要遍历所有
Segment进行累加,且当统计过程中modCount发生变化时需要对所有Segment加锁重新统计【效率低】
- jdk1.8
- 数据结构是Node+链表/红黑树
- 使用
CAS+synchronized控制并发,锁的粒度是单个Node
- 默认数组容量是16(一定是2的幂),默认的负载因子是0.75
- 当链表长度达到8且数组的长度大于等于64时,会将链表转化成红黑树,但如果数组长度没有达到64,会进行扩容。而在删除节点时如果树节点的个数小于等于6则会将树结构退化为链表。
- 扩容时可以多线程并发扩容,每个线程分别负责一组桶进行扩容,因为扩容后旧的桶中的元素要么在原来位置的桶中,要么在原来位置+旧桶容量的新位置(这依托于数桶数组的大小一定是2的幂,在rehash时只需要判断多出的最高位是0还是1,即可知道该元素的位置,使用位运算判断rehash的位置相较于重新计算hash也提高了rehash的计算效率)。所以多线程并发控制不同的桶进行扩容不会产生冲突,可以提高扩容效率。
- key和value都不允许为
NULL
- size()方法统计元素数量,引入了
CounterCell和LongAdder统计元素数量,更高效且无需加锁。当更新元素数量时,可以让每个线程更新自己持有的CounterCell,最终统计所有CounterCell得到总的元素数量,当线程竞争比较激烈时,即CounterCell不够用的时候,LongAdder会继续扩容出CounterCell给线程使用。
- put()插入流程
- (1)判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
(2)根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向(6),如果table[i]不为空,转向(3);
(3)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向(4),这里的相同指的是hashCode以及equals;
(4)判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向(5);
(5)遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
(6)插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
- get()获取key对应的value流程
- 首先根据hash找到桶
- 如果桶不为空则根据hash、地址、equals判断是否与头结点相同,相同则直接返回
- 否则,判断头结点hash值是否小于0,如果小于0则表明正在扩容或者桶中是红黑树结构,使用find方法查找。如果正在扩容会在旧桶和新桶中都进行查找(不用加锁,且避免遗漏),如果是树则在树中查找。
- 否则,顺着链表查找。
HashTable
- 线程安全,使用
synchronized关键字给方法加锁控制并发。
- key和value都不允许为
NULL
- 默认容量11,默认负载因子0.75,达到阈值扩容为原容量的2n+1。
- 哈希冲突采用拉链法解决,尾插法
- 使用JMH对
ConcurrentHashMap和HashTable进行性能测试,8个线程并发,性能差距还是比较大的
- 测试环境
CPU:12th Gen Intel(R) Core(TM) i5-12400
RAM:62G
- 每个线程:存储的同时读取一百万个简单元素
ConcurrentHashMap:401.407 ms/op
HashTable: 3159.129 ms/op
HashSet
- 底层数据结构是
HashMap(数组+链表),以Key存储元素值,Value设置定固定的对象。
- 不可重复,无序,线程不安全
- 支持存储
NULL
LinkedHashSet
- 底层数据结构是
LinkedHashMap(数组+链表+双向链表),以Key存储元素值,Value设置定固定的对象。
- 不可重复,按照插入顺序进行遍历,线程不安全
- 支持存储
NULL
TreeSet
- 底层数据结构是
TreeMap(红黑树),以Key存储元素值,Value设置定固定的对象。
- 不可重复,按照存储
Comparator或者存储对象的Comparable定义的比较规则进行排序
- 因为需要可比较,所以不支持存储
NULL
- 线程安全的
Set
- 通过
Collections.synchronizedSet() 方法包装一个线程不安全的 Set
CopyOnWriteArraySet
- 基于
CopyOnWriteArrayList 实现,允许存储NULL
ConcurrentSkipListSet
- 底层数据结构是
ConcurrentSkipListMap,基于跳表实现,不允许存储NULL
- LeetCode 33.搜索旋转排序数组
- 考察二分查找