hashmap、hashtable和concurrenthashmap
hashMap
- 继承自AbstarctMap类
- 底层数据结构是:数组+链表+红黑树
- key和value都允许为null。key为null的键值对永远都放在以table[0]为头结点的链表中。
- 初始容量为16,扩容是乘以2。
- 线程不安全的键值对存储结构,当有多线程同时修改的时候可能会形成环结构,从而造成死锁问题。
为什么容量要总是2的次幂:
- 用与运算代替取模运算,速度快
- 为了散列更均匀,利用0&1 = 0, 1&1 = 1的特性
- 易于计算扩容后元素在数组索引位置,不是在原下标就是在原下标+扩容前的容量
hashtable
- 继承自Dictionary类
- key和value都不支持为null
- 初始容量为11,扩容乘以2+1
- 线程安全,每个方法都添加了synchronized
- 与hashmap的hashcode计算方法不同,hashtable直接使用hash函数计算的值,而hashmap还需要对计算得到的hashcode再取模运算
- 除迭代器外,还支持elements()用于枚举其value的值,多了一个contains()方法,与containsValue()的功能类似
concurrentmap
hashmap的线程不安全性
hashtable线程安全但是效率低
JDK1.7
hsahmap在JDK1.7的数据结构是:数组+链表
- ConcurrentHashMap使用分段锁,它是由segment数组结构和HashEntry数组结构构成的,其中,segment是一种可重入锁,而HashEntry是键值对的数据存储结构。
- Segment结构:每个segment包含一个HashEntry数组,它是链表结构的元素,segment就是这个hashentry的锁。
- 初始化segment数组
segment数组的长度ssize是通过concurrencyLevel计算的,是大于等于concurrencyLevel(默认为16)的最小的2的n次幂。这是为了能够通过按位与计算出segments数组的下标索引。
sshift是ssize从1向左移位的次数,默认为4。segmentshift用于定位参与散列运算的位数,等于32-sshift;而segmentMask是散列运算的掩码,等于ssize-1; - 初始化每个segment
inittial是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在初始化Segment时,也定义了一个中间变量cap,其等于initialCapacity除以ssize的倍数c,如果c大于1,则取大于等于c的2的N次方,cap表示Segment中HashEntry数组的长度;loadFactor表示了Segment的加载因子,通过cap*loadFactor获得每个Segment的阈值threshold.
默认情况下,initialCapacity等于16,loadFactor等于0.75,concurrencyLevel等于16,threshold等于0. - 定位segment
concurrentMap对元素的hashcode进行再散列,旨在散列更均匀来减少散列冲突。如果元素散列后都在同一个segment上面,其实也就失去了分段锁的意义。 - 分段锁的操作:
- get()操作:经过再散列,定位Segment –> 然后根据该散列值通过散列运算定位HashEntry –> 通过getObjectVolatile()方法获取指定偏移量上的HashEntry –> 通过循环遍历链表获取对应值。
get是不加锁的,因为concurrenthashmap上get方法用到的共享变量都是volatile变量,保证了多线程间的可见性。 - put()操作:需要加锁。首先定位到segment,然后在segment里进行插入操作。插入操作分为两个步骤:
1)判断是否需要扩容:插入元素前进行扩容判断,分段式扩容,扩容乘以2。
2)定位需要添加元素的位置,然后将其放在hashEntry数组里。 - size()操作:每个segment的count是一个volatile变量,但是在累加的过程中,前面的segment的count可能还是发生了变化。ConcurrentHashMap先尝试两次不加锁的累计统计,定义一个modcount的变量,如果有put,remove或者clean的操作时,会对这个参数进行加1,通过比较modcount累加前和累加后的值有误变化来判断size()是否发生了变化,如果两次尝试失败,就会对所有的put、remove和clean方法加锁再统计容器的size()。
- get()操作:经过再散列,定位Segment –> 然后根据该散列值通过散列运算定位HashEntry –> 通过getObjectVolatile()方法获取指定偏移量上的HashEntry –> 通过循环遍历链表获取对应值。
JDK1.8
jdk1.8之后的hashmap的底层数据结构是:数组+链表+红黑树
- concurrentMap使用CAS+synchronized关键字实现。
- concurrentHashMap的初始化
通过指定的初始容量initialCapacity,加载因子loadFactor和预估并发度concurrencyLevel三个参数计算table数组的初始大小sizeCtl的值。
sizeCtl是一个控制标识符,不同的值代表不同的意思:其为0时,表示hash表还未初始化,而为正数时这个数值表示初始化或下一次扩容的大小,相当于一个阈值;即如果hash表的实际大小>=sizeCtl,则进行扩容,默认情况下其是当前ConcurrentHashMap容量的0.75倍;如果为-1,表示正在进行初始化操作;为-N时,则表示有N-1个线程正在进行扩容。
在构造ConcurrentHashMap时,并不会对hash表(Node<K, V>[] table)进行初始化,hash表的初始化是在插入第一个元素时进行的。