ConcurrentHashMap
ConcurrentHashMap 和 Hashtable 的区别
主要提醒在实现线程安全的方式上不同
- 底层数据结构
- JDK1.7 的ConcurrentHashMap 底层采用 分段数组 + 链表 实现, JDK 1.8 采用数据结构与HashMap 1.8 的结构一样, 数组 + 链表/红黑树, Hashtable 底层数据结构采用 数组 + 链表 形式
- 实现线程安全的方式
- ConcurrentHashMap
- 在JDK 1.7 时, ConcurrentHashMap (分段锁)对整个桶数组进行了分割分段(segment), 每把锁只锁容器其中一部分数据, 多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率.
- 在JDK 1.8 时, 直接用 Node 数组 + 链表 + 红黑树的数据结构来实现, 并发控制使用 synchronized 和CAS 来操作, 整体看起来就像是优化过且线程安全的HashMap
- Hashtable
- 使用 synchronized 来保证线程安全,效率非常低,当一个线程访问同步方法时, 其他线程也访问同步方法,可能会阻塞或轮询整体.当put元素,另一个线程不能使用put方法添加元素,也不能使用get. 竞争会越来越激烈,效率越低
- ConcurrentHashMap
加锁方式
Hashtable
ConcurrentHashMap
JDK1.7 ConcurrentHashMap
加锁原理
首先将数据分宜一段一段的存储, 然后给每一段数据分配一把锁, 当一个线程占用锁访问其中一段数据时, 其他段的数据也能被其他线程访问.
ConcurrentHashMap 是由 Segment 数据结构 + HashEntry 数据结构组成
Segment 实现了 ReentrantLock, 是一种可重入锁, 扮演锁的角色, HashEntry 用于存储数据
一个ConcurrentHashMap中包含一个 Segment 数组, Segment中又包含一个HashEntry数组,当对HashEntry数组中的数据进行修改是,必须获取对应的Segment 锁
Segment 是 ConcurrentHashMap 的一个内部类
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
// 大小
transient int threshold;
// 负载因子
final float loadFactor;
}
使用了 volatile 修饰
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
每当一个线程占用锁访问一个 Segment 时, 不会影响到其他的 Segment
如果容量大小是16,那么他的并发度就是16, 可以同时允许16个线程操作16个 Segment 且线程安全
put() 方法逻辑
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();//这就是为啥他不可以put null值的原因
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
//释放锁
unlock();
}
return oldValue;
}
首先第一波会尝试获取锁, 如果获取失败就说明有其他线程存在竞争, 利用 scanAndLockForPut() 自旋获取锁, 如果充实的次数到达了MAX_SCAN_RETRIES则改为阻塞锁获取,保证能获取成功
get()方法逻辑
获取元素只需要将 key 通过 hash之后定位到具体的 Segment, 在通过一次hash 定位到具体的元素
由于 HashEntry 中 value 属性使用 volatile 修饰, 保证内存的可见性, 所以每次获取都是最新值, 获取过程不需要加锁
存在的问题
因为基本上都是数组 + 链表 形式, 查询的时候需要遍历链表, 会导致效率很低, 与 JDK 1.7 的HashMap 一样, 在 JDK 1.8 中优化
JDK1.8 ConcurrentHashMap
加锁原理
取消了Segment 分段锁, 采用CAS + Synchronized 来保证并发安全, 数据结构与 HashMaori 1.8 结构类似, 数组 + 链表 / 红黑树. 在链表长度超过阈值8是将链表转换为红黑树. 把之前的 HashEntry 改成 Node, 作用不变, 把值和 next 采用了 volatile 修饰, 保证可见性
Synchronized 只锁定当前链表或红黑树的首节点, 这样只要hash不冲突, 就不会产生并发,效率又提升
put() 方法逻辑
- 根据 key 计算出 hashcode
- 判断是否需要初始化
- 即为当前 key 定位出的 Node, 如果为空表示当前位置可以写入数据, 利用 CAS 尝试写入, 失败则自旋保证成功
- 如果当前位置的 hashcode == MOVED == -1 , 则需要进行火绒
- 如果都不满足, 则利用 synchronized 锁写入数据
- 如果数量大于 TREEIFY_THRESHOLD 则转化为红黑树
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//根据 key 计算出 hashcode
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//判断是否需要进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//为当前 key 定位出的 Node, 如果为空表示当前位置可以写入数据, 利用 CAS 尝试写入, 失败则自旋保证成功
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//如果当前位置的 hashcode == MOVED == -1 则需要进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//如果都不满足, 利用 synchronized 锁写入数据
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//如果数量大于 TREEIFY_THRESHOLD 则转化为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
get() 方法逻辑
- 根据计算出来的 hashcode 值确定节点位置
- 如果是搜索到的节点 key 与传入的 key 相同且不为null, 直接返回这个节点
- 如果是红黑树则按照树的方法获取值
- 都不满足则按照链表方法遍历获取值
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
//根据计算出来的 hashcode 值确定节点位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//如果是搜索到的节点 key 与传入的 key 相同且不为null, 直接返回这个节点
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//说明该节点在树上, 根据树方法寻址
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//都不满足则按照链表放肆遍历获取值
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}