1、它实现了ConcurrentMap
接口,该接口定义了一些原子操作约定
2、线程安全
- 完全的并发读和高并发写
- 读操作完全无锁,牺牲了一致性;写操作部分有锁
- 它与
HashTable
、Collections.synchronizedMap
HashMap
支持null
,ConcurrentHashMap
、HashTable
不支持null
3、java7
- 分段锁
- 哈希表/链表
4、java8
CAS
+Unsafe
- 哈希表/链表 + 红黑树
java7
的实现
一、相关概念
1、分段锁
ConcurrentHashMap
底层采用多个分段Segment
,每段下面都是一个哈希表,这就是分段。每当需要对每段数据上锁操作时,只需要对Segment
上锁即可,这就是分段锁。通常称Segment
的数量叫做并发度concurrency
。
优点:
- 在上锁的情况下,提高了并发度;
2、并发度concurrency
1 | /** |
这表示默认情况下,会有16个段
Segment
3、每个Segment
的哈希表长度都是2的幂次方
在ConcurrentHashMap
构造方法中
二、源码分析
1、get
方法
- 计算
segment
的位置 - 找到这个段下面的哈希表
- 遍历链表,看是否存在(1)为什么要使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
// 获取到key所在Segment数组的下标
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// 判断这个下标是否存在,以及Segment下面的哈希表是否存在
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 熟悉的:(tab.length - 1) & h操作
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}UNSAFE.getObjectVolatile(segments, u)
这种方式来读取数组下标的某个元素?
提高性能。使用常用segments[i]
这种语法,在编译字节码的时候,是会检查数组是否越界;而使用上面的代码,会节省这一步。
(2)如何保证线程安全性?
即如何保证在多线程环境下,当线程在做更新操作时,如果其他线程在同步读的话,是可能出现脏数据、空指针情况。那么ConcurrentHashMap
是如何保证的?ConcurrentHashMap
为了提高高并发,而牺牲了一致性,但这种一致性是弱一致性,不会对程序造成大的过错。所以脏数据是无法避免的,因此在java8
的类注释写到不建议使用size
、isEmpty
、containsValue
来进行判断语句。
1 | * Bear in mind that the results of aggregate status methods including |
2、put
方法
- 找到
Segment
,必要时新建; Segment
执行put
操作,必要时扩容;1
2
3
4
5
6
7
8
9
10
11public 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 // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
(1)扩容时如何保证线程安全性?
- 在创建
Segment
时,采用CAS
保证线程安全性; - 在创建
Entry
时,因为Segment
本身就是ReentrantLock
,在其Segment.put()
方法是一定保证在获取到锁的情况下才执行操作的;
(2)Unsafe.getObject()
的作用?
java8
的实现
一、与java7
的改进
使用哈希表 + 链表/红黑树 的数据结构
放弃使用分段锁,改用CAS
、volatile
、Unsafe
java7
的分段锁很好,但锁毕竟还是很慢的,所以java8
实现了尽可能地无锁环境。
这里所说地无锁也仅仅大多数情况下,在某些特殊场景还是需要锁地。
锁的粒度更细
java7
锁地粒度是Segment
,而在java8
中锁地粒度是每个Entry
二、源码分析
1、get
方法
1 | public V get(Object key) { |
(1)查找没有锁,如何有人在写入怎么办?
- 在红黑树状态下,查找是有读写锁;
- 在链表状态下,跟
java7
相似,牺牲了弱一致性;
(2)红黑树是怎么找的?
2、put
方法
1 | final V putVal(K key, V value, boolean onlyIfAbsent) { |
(1)在哪里扩容的?
(2)扩容是如何进行地?