文章目录
ConcurrentHashMap
@author 无忧少年
@createTime 2021/03/07
1. ConcurrentHashMap和HashMap、HashTable之间的区别
- HashMap不是并发安全的容器,在并发情况下使用put操作会引起死循环,原因为多线程会导致HashMap的Entry链表形成环形的数据结构,一旦形成环形数据结构,Entry的next节点就永不为空,就会产生死循环获取Entry,并发情况下put值得时候可能会互相覆盖,一个线程遍历,另一个线程修改,会触发fast-fail。
- HashTable在并发情况下效率低下,HashTable是使用的全局锁,当一个线程访问HashTable的同步方法时,其他访问同步方法的线程均会阻塞,在线程竞争激烈的情况,效率非常低。
- ConcurrentHashMap的锁分段技术可有效提升并发访问效率,HashTable效率低下的主要原因是因为所有访问HashTable的线程都是竞争同一把锁,如果说容器中有很多把锁,并把锁应用于一部分数据上,那么多线程访问容器里的不同数据段的数据时,各个线程之间就不会存在锁的竞争,从而提高并发访问效率,这就是ConcurrentHashMap的锁分段技术。首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个县城占用锁访问一段数据的时候,其他段的数据也能被其他县城访问。
2. ConcurrentHashMap的结构
// node数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
private static final int DEFAULT_CAPACITY = 16
//数组可能最大值,需要与toArray()相关方法关联
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
private static final float LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MIN_TRANSFER_STRIDE = 16;
private static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED = -1;
// 树根节点的hash值
static final int TREEBIN = -2;
// ReservationNode的hash值
static final int RESERVED = -3;
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transient volatile Node<K,V>[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
*当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
*当为0时:代表当时的table还没有被初始化
*当为正数时:表示初始化或者下一次进行扩容的大小
*
private transient volatile int sizeCtl;
3. put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//1.校验参数是否合法
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
//2.遍历Node
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//2.1.Node为空,初始化Node
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//2.2.CAS对指定位置的节点进行原子操作
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//2.3.如果Node的hash值等于-1,map进行扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//2.4.如果Node有值,锁定该Node。
//如果key的hash值大于0,key的hash值和key值都相等,则替换,否则new一个新的后继Node节点存放数据。
//如果key的hash小于0,则考虑节点是否为TreeBin实例,替换节点还是额外添加节点。
else {
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) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//3.计算map的size
addCount(1L, binCount);
return null;
}
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
f
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。
4. get 方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
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;
}
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 不满足那就按照链表的方式遍历获取值。
5.size方法
HashMap的size()是在put的时候对size进行++size的操作,每增加一个元素size大小自增1,调用size()的时候,直接赋值即可获得。
ConcurrentHashMap的计算map中映射的个数,可以由mappingCount完全替代,因为ConcurrentHashMap可能包含的映射数多于返回值为为int的映射数。该返回的值是估计值,实际数量可能会因为并发插入或删除而有所不同。
ConcurrentHashMap的size方法是一个估计值,并不是准确值。
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}
————————————————