为什么HashTable慢
- HashTable 虽然它是一个线程安全的类,但是它是使用 synchronized 关键字来修饰一些操作。
- synchronized 锁住了一整个哈希表。
- 导致同一时期只有一个线程可以进行对哈希表的操作。
- 所以 HashTable 虽然线程安全但是它慢,一个人操作所有人等待,并发值就是1。
ConcurrentHashMap
- 在 JDK1.5 ~JDK1.7 中,ConcurrentHashMap 这个类它使用的是分段锁的机制来实现并发控制的。
数据结构
- 分段锁可以这么理解,我们原来的哈希表又在最外层套了一个数组,这个数组就被称作为 段(Segment)。
- 段数组中每个桶都包含了一个 HashMap 的哈希表,该哈希表在 JDK1.8 之前统一使用数组+链表的形式。
- 段数组中的每个段都可以独立加锁,段的默认大小为 2^4 ,也就是默认可以有16个线程进行并发编程。
- Segment 通过继承 ReentrantLock 来进行加锁。
- 只要保证段中桶的线程安全,那么整个段也就是整个 ConcurrentHashMap 对象是线程安全的了。
- 值得注意的是:Segment数组的长度是不可以被改变的,初始化如果不规定,那么就采用默认的 2^4
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9OyrYIw-1643030217441)(C:\Users\noblegasesgoo\AppData\Roaming\Typora\typora-user-images\image-20211106094938292.png)]](https://i-blog.csdnimg.cn/blog_migrate/cc6e76ee15d649c5c3f7092ea6b07c91.png)
初始化
- 两个参数:
- initialCapacity :
- 初始容量,这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。
- 比如你初始容量是 64,Segment 的容量为16,那么每个段中哈希表的初始容量就为 64/16=4。
- loadFactor:
- 其中某个构造函数的源码详解:
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
- 如果是无参构造的话,我们可以从中看到段中哈希表的 默认大小为2,负载因子是0.75,那么 扩容阈值就是1.5,插入第一个元素不会扩容。
- 那么到这整个 ConcurrentHashMap 对象就创建完毕了,但是有一个疑问:
- 在末尾初始化了段表中的第一个位置,其他位置没有初始化,这是为什么呢?
- 是因为每次创建一个 **Segment **对象要计算好几个值,初始化 **ConcurrentHashMap **的时候初始化了一个s0,只有再要初始化的 Segment 对象的时候,就拿s0当模板直接照搬参数就行,这样就会快一点。
put方法及其过程分析
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensur