1【理解】
ConcurrentHashMap:检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和remove)重叠,ConcurrentHashMap跟Hashtable类似但不同于HashMap,它不可以存放空值,key和value都不可以为null【null值用来判断是否需要加锁立即重试】。
ConcurrentHashMap从JDK1.5开始随java.util.concurrent包一起引入JDK中,在JDK8以前,ConcurrentHashMap都是基于Segment分段锁来实现的,在JDK8以后,就换成synchronized和CAS这套实现机制了
JDK1.8中的ConcurrentHashMap中仍然存在Segment这个类,而这个类的声明则是为了兼容之前的版本序列化而存在的。
JDK1.8中的ConcurrentHashMap不再使用Segment分段锁,而是以table数组的头结点作为synchronized的锁。和JDK1.8中的HashMap类似,对于hashCode相同的时候,在Node节点的数量少于8个时,这时的Node存储结构是链表形式,时间复杂度为O(N),当Node节点的个数超过8个时,则会转换为红黑树,此时访问的时间复杂度为O(long(N))。
2 【保证线程安全】
● 一、使用volatile保证当Node中的值变化时对于其他线程是可见的
【Node中的val和next都被volatile关键字修饰。我们改动val的值或者next的值对于其他线程是可见的,因为volatile关键字,会在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。】
【ConcurrentHashMap提供类似tabAt来读取Table数组中的元素,这里是以volatile读的方式读取table数组中的元素,主要通过Unsafe这个类来实现的,保证其他线程改变了这个数组中的值的情况下,在当前线程get的时候能拿到。】【】
【而与之对应的,是setTabAt,这里是以volatile写的方式往数组写入元素,这样能保证修改后能对其他线程可见。】
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
3 【使用table数组的头结点作为synchronized的锁来保证写操作的安全】
【当头结点不为null时,则使用该头结点加锁,这样就能多线程去put hashCode相同的时候不会出现数据丢失的问题。synchronized是互斥锁,有且只有一个线程能够拿到这个锁,从而保证了put操作是线程安全的。】
4 【当头结点为null时,使用CAS操作来保证数据能正确的写入】
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
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();
//当头结点为null,则通过casTabAt方式写入
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
}
else if ((fh = f.hash) == MOVED)
//正在扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//头结点不为null,使用synchronized加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
//此时hash桶是链表结构
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
//当链表结构大于等于8,则将链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
【所谓的CAS,即compareAndSwap,执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。】
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
【 asTabAt同样是通过调用Unsafe类来实现的,调用Unsafe的compareAndSwapObject来实现,其实如果仔细去追踪这条线路,会发现其实最终调用的是cmpxchg这个CPU指令来实现的,这是一个CPU的原子指令,能保证数据的一致性问题。】
1.8和之前的版本比较 参考 【ConcurrentHashMap 1.7】 理解性知识整 blog.csdn.net/qfzhangwei/…
旧版本的一个segment锁,保护了多个hash桶,而jdk8版本的一个锁只保护一个hash桶,由于锁的粒度变小了,写操作的并发性得到了极大的提升。
【更多的扩容线程】
扩容时,需要锁的保护。因此:旧版本最多可以同时扩容的线程数是segment锁的个数。
而jdk8的版本,理论上最多可以同时扩容的线程数是:hash桶的个数(table数组的长度)。但是为了防止扩容线程过多,ConcurrentHashMap规定了扩容线程每次最少迁移16个hash桶,因此jdk8的版本实际上最多可以同时扩容的线程数是:hash桶的个数/16,每个线程至少迁移16个桶。
【扩容期间,依然保证较高的并发度】
旧版本的segment锁,锁定范围太大,导致扩容期间,写并发度,严重下降。
而新版本的采用更加细粒度的hash桶级别锁,扩容期间,依然可以保证写操作的并发度。
【ConcurrentHashMap的重要结构与方法】
ConcurrentHashMap内部,和hashmap一样,维护了一个table数组,数组元素是Node链表或者红黑树.
【关于table数组,有3个重要方法】
//以volatile读的方式读取table数组中的元素
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
//以volatile写的方式,将元素插入table数组
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
//以CAS的方式,将元素插入table数组
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
//原子的执行如下逻辑:如果tab[i]==c,则设置tab[i]=v,并返回ture.否则返回false
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
旧版本对table数组元素的读写,都是在segment锁保护的情况下进行的,因此不会内存可见性问题。而jdk8的实现中,锁的粒度是hash桶,因此对table数组元素的读写,大部分都是在没有锁的保护下进行的,那么该如何保证table数组元素的内存可见性?【线程重试性代价小于----线程上下文交换的代价】 volatile
ConcurrentHashMap中的锁是hash桶的头结点,那么当多个put线程访问头结点为空的hash桶时,在没有互斥锁保护的情况下,多个put线程都会尝试将元素插入头结点,此时如何确保并发安全呢? CAS
6 【应用】
在哪里用的?
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
然后,我们看下ConcurrentHashMap的put方法是如何通过CAS确保线程安全的:
假设此时有2个put线程,都发现此时桶为空,线程一执行casTabAt(tab,i,null,node1),此时tab[i]等于预期值null,因此会插入node1。随后线程二执行casTabAt(tba,i,null,node2),此时tab[i]不等于预期值null,插入失败。然后线程二会回到for循环开始处,重新获取tab[i]作为预期值,重复上述逻辑。
final V putVal(K key, V value, boolean onlyIfAbsent) {
...
for (Node<K,V>[] tab = table;;) {
...
//key定位到的hash桶为空
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//cas设置tab[i]的头结点。
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; //设置成功,跳出for循环
//设置失败,说明tab[i]已经被另一个线程修改了。回到for循环开始处,重新判断hash桶是否为空。如何往复,直到设置成功,或者hash桶不空。
}else{
synchronized (f) {
//
}
}
}
...
}
CAS的其他应用
//JDK7版本的 AtomicInteger 类的原子自增操作
public final int getAndIncrement() {
for (;;) {
//获取value
int current = get();
int next = current + 1;
//value值没有变,说明其他线程没有自增过,将value设置为next
if (compareAndSet(current, next))
return current;
//否则说明value值已经改变,回到循环开始处,重新获取value。
}
}
get方法
get方法同样利用了volatile特性,实现了无锁读。
查找value的过程如下:
1. 根据key定位hash桶,通过tabAt的volatile读,获取hash桶的头结点。
2. 通过头结点Node的volatile属性next,遍历Node链表
3. 找到目标node后,读取Node的volatile属性val
可见上述3个操作都是volatile读,因此可以做到在不加锁的情况下,保证value的内存可见性
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 &&
//定位目标hash桶,通过tabAt方法valatile读,读取hash桶的头结点
(e = tabAt(tab, (n - 1) & h)) != null) {
//第一个节点就是要找的元素
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
//e.val也是valatile
return e.val;
}
//特殊节点(红黑树,已经迁移的节点(ForwardingNode)等
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//遍历node链表(e.next也是valitle变量)
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
put方法
2. 由于锁的粒度是hash桶,多个put线程只有在请求同一个hash桶时,才会被阻塞。请求不同hash桶的put线程,可以并发执行。
4. put线程,请求的hash桶为空时,采用for循环+CAS的方式无锁插入。
remove方法
如图所示:删除的node节点的next依然指着下一个元素。此时若有一个遍历线程正在遍历这个已经删除的节点,这个遍历线程依然可以通过next属性访问下一个元素。从遍历线程的角度看,他并没有感知到此节点已经删除了,这说明了ConcurrentHashMap提供了弱一致性的迭代器。遍历操作可以参考ConcurrentHashMap源码分析(JDK8) 遍历操作分析
public V remove(Object key) {
return replaceNode(key, null, null);
}
/**
参数value:当 value==null 时 ,删除节点 。否则 更新节点的值为value
参数cv:一个期望值, 当 map[key].value 等于期望值cv 或者 cv==null的时候 ,删除节点,或者更新节点的值
*/
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//table还没有初始化或者key对应的hash桶为空
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
//正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) {
//cas获取tab[i],如果此时tab[i]!=f,说明其他线程修改了tab[i]。回到for循环开始处,重新执行
if (tabAt(tab, i) == f) {
//node链表
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
//找的key对应的node
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
//cv参数代表期望值
//cv==null:表示直接更新value/删除节点
//cv不为空,则只有在key的oldValue等于期望值的时候,才更新value/删除节点
//符合更新value或者删除节点的条件
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
//更新value
if (value != null)
e.val = value;
//删除非头节点
else if (pred != null)
pred.next = e.next;
//删除头节点
else
//因为已经获取了头结点锁,所以此时不需要使用casTabAt
setTabAt(tab, i, e.next);
}
break;
}
//当前节点不是目标节点,继续遍历下一个节点
pred = e;
if ((e = e.next) == null)
//到达链表尾部,依旧没有找到,跳出循环
break;
}
}
//红黑树
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
//如果删除了节点,更新size
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
7 【图示】