JUC系列并发容器之ConcurrentHashMap(JDK1.8版)
Posted 顧棟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC系列并发容器之ConcurrentHashMap(JDK1.8版)相关的知识,希望对你有一定的参考价值。
ConcurrentHashMap(JDK1.8)
文章目录
在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap。在使用中发现最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。
组成
内部类
Node
拥有键值对的链表的基础存储结构
static class Node<K,V> implements Map.Entry<K,V>
// key的hash值
final int hash;
// 关键字
final K key;
// 存储值
volatile V val;
// 下一个结点
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next)
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
public final K getKey() return key;
public final V getValue() return val;
public final int hashCode() return key.hashCode() ^ val.hashCode();
public final String toString() return key + "=" + val;
public final V setValue(V value)
throw new UnsupportedOperationException();
public final boolean equals(Object o)
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k)
Node<K,V> e = this;
if (k != null)
do
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
while ((e = e.next) != null);
return null;
ForwardingNode
在transfer操作期间插入到 bin 头部的节点。
static final class ForwardingNode<K,V> extends Node<K,V>
// node数组,存放的什么?
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab)
super(MOVED, null, null, null);
this.nextTable = tab;
Node<K,V> find(int h, Object k)
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;)
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;)
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0)
if (e instanceof ForwardingNode)
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
else
return e.find(h, k);
if ((e = e.next) == null)
return null;
TreeNode
作为红黑树结构的存储结构,比一般红黑树存储结构出来的next和prev,可以将这些结点变成双向的链表结构,是为了方便从链表变为红黑树,在从红黑树变成链表。在ConcurrentHashMap中,链表与红黑树的转变是依据链表中的结点数量,默认变成红黑树的链表结点个数需要大于8。
hash | key | val | next | prev | parent | left | right | red |
---|---|---|---|---|---|---|---|---|
key的hash值 | 关键字 | 具体值 | 下一个结点 | 上一个结点 | 双亲结点 | 左子结点 | 右子结点 | 结点颜色 |
static final class TreeNode<K,V> extends Node<K,V>
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent)
super(hash, key, val, next);
this.parent = parent;
Node<K,V> find(int h, Object k)
return findTreeNode(h, k, null);
/**
* Returns the TreeNode (or null if not found) for the given key
* starting at given root.
*/
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc)
if (k != null)
TreeNode<K,V> p = this;
do
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
while (p != null);
return null;
TreeBin
继承Node,树标记结构,表明该结点背后有一棵红黑树。在新增结点和删除结点的时候,为了并发的安全都需要会进行锁的竞争。root和first不应该是同一个结点。
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
为了减少本篇篇幅长度,将红黑树的实现部分拆分出去:ConcurrentHashMap(JDK1.8)中红黑树的实现
数据结构
核心方法
方法名 | 描述 |
---|---|
V putVal(K key, V value, boolean onlyIfAbsent) | 将指定键映射到此表中的指定值。 键和值都不能为空。 可以通过使用与原始键相同的键调用 get 方法来检索该值。 onlyIfAbsent :true 代表 不更新旧值,false 更新旧值 |
Node<K,V>[] initTable() | 使用 sizeCtl 中记录的大小初始化表。第一级结点存储的数组结构 |
void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) | 将每个 bin 中的节点移动和/或复制到新表。 |
void treeifyBin(Node<K,V>[] tab, int index) | 在给定的索引中替换所有链接节点将简单的链表改为红黑树,除非表太小(数组长度小于64),在这种情况下对Node数组进行扩容,扩2倍 |
void tryPresize(int size) | 尝试预先调整表格大小以容纳给定数量的元素。即将Node数组扩容到指定的size大小。 |
void addCount(long x, int check) | 添加到计数中,如果Node数组太小并且尚未调整大小,则启动transfer。 如果已经调整大小,则在工作可用时帮助执行扩容(多线程扩容,在线程数量允许的情况下,一起进行扩容操作,扩容线程已满的话,自旋重新检查)。 转移后重新检查占用情况,以查看是否已经需要再次调整大小,因为调整大小滞后于添加。 |
V get(Object key) | 返回指定键映射到的值,如果此映射不包含该键的映射,则返回 null。 更正式地说,如果此映射包含从键 k 到值 v 的映射,使得 key.equals( k),则此方法返回 v; 否则返回null。 (最多可以有一个这样的映射。) |
V replaceNode(Object key, V value, Object cv) | 四种公共删除/替换方法的实现:用 v 替换节点值,如果非空,则以 cv 匹配为条件。 如果value为空,则删除。 |
int spread(int h)
计算hash值
static final int HASH_BITS = 0x7fffffff; // 正常节点哈希的可用位,十六进制表示法,一个十六进制数占4个bit。int一个32位(bit),那需要8个十六进制数,故可以理解为0x7 f f f f f f f,F的二进制为1111,7的二进制0111.
static final int spread(int h)
return (h ^ (h >>> 16)) & HASH_BITS;
构造函数
ConcurrentHashMap(int initialCapacity)
创建一个新的空映射,其初始表大小可容纳指定数量的元素,无需动态调整大小。
参数
initialCapacity
:初始容量。
public ConcurrentHashMap(int initialCapacity)
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
根据给定的元素数 (initialCapacity)、表密度 (loadFactor) 和并发更新线程数 (concurrencyLevel) 创建一个新的空映射,其初始表大小。
参数
initialCapacity
:初始容量。 给定指定的负载因子,实现执行内部大小调整以适应这么多元素。loadFactor
:用于建立初始表大小的负载因子(表密度)。concurrencyLevel
:估计的并发更新线程数。 实现可以使用这个值作为大小提示。
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 若出现初始容量小于并发数,则以并发数的值作为初始容量
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
int tableSizeFor(int c)
计算数组的大小,通过初始值*1.5+1,然后向上取2的n次方。
private static final int tableSizeFor(int c)
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
PUT过程
putVal(K key, V value, boolean onlyIfAbsent)
public V put(K key, V value)
return putVal(key, value, false);
final V putVal(K key, V value, boolean onlyIfAbsent)
if (key == null || value == null) throw new NullPointerException();
// 计算hash值
int hash = spread(key.hashCode());
// 对应链表长度???
int binCount = 0;
// 存放数据的Node<K,V>数组
for (Node<K,V>[] tab = table;;)
Node<K,V> f; int n, i, fh;
// 若Node<K,V>[]还是null或没有元素,则需要进行初始化
if (tab == null || (n = tab.length) == 0)
// 初始化数组
tab = initTable();
// 至此代表容器中的数组已经成功初始化,找该 hash 值对应的数组下标,得到第一个节点 f;
// 若f为null,使用CAS操作直接将新节点放到数组下标i的位置,执行成功直接退出for,执行失败代表存在竞争,继续执行
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
// MOVED代表在扩容
else if ((fh = f.hash) == MOVED)
// 帮助数据迁移
tab = helpTransfer(tab, f);
else
// 此时数组当前下标的元素的头结点f,不为空
V oldVal = null;
// 对这个位置的头结点上锁
synchronized (f)
if (tabAt(tab, i) == f)
// 对此头结点的hash进行判断,若值>=0则说明是数组此下标的元素是个链表
if (fh >= 0)
// 链表节点计数,由头结点从1开始
binCount = 1;
// 遍历链表
for (Node<K以上是关于JUC系列并发容器之ConcurrentHashMap(JDK1.8版)的主要内容,如果未能解决你的问题,请参考以下文章
JUC系列并发容器之ConcurrentLinkedQueue(JDK1.8版)
JUC系列并发容器之ConcurrentHashMap(JDK1.8版)