存储结构
分为多个segment,每个segment和hashmap结构一样(一个table+拉链),
每个segment单独管理自己的count。
类声明
public class ConcurrentHashMap extends AbstractMap
implements ConcurrentMap, Serializable {
}
concurrentmap
提供几个复合源自操作:
- putIfAbsent(K,V), 没有则添加
- remove(K,V) 有条件删除,如果待删除的key的值和传入的V一致,则删除
- replace(K,V), 如果有值,才替换(和putIfAbsent相反)
- replace(K,oldV,NewV) 。如果有值且旧值等于oldV才替换
Segment
每个segment都是一个独立的锁,独立维护自己的count,拥有和hashmap差不多的基本操作,一般的get,put,remove等操作,都是委托segment进行的。
static final class Segment extends ReentrantLock implements Serializable { //继承自ReentrantLock
transient volatile int count; // 独立维护的count
transient int modCount;
transient int threshold;
transient volatile HashEntry[] table;
final float loadFactor;
}
segment的加入使得concurrenthashmap实现了锁分离,默认并发数为16,光这一点就足以使得它比hashmap并发性能高出十几倍。
concurrenthashmap的get不加锁却能保证线程安全
Segment中具体数据是维护在一个HashEntry
/*
非典型不可变对象(value可变)
*/
static final class HashEntry {
final K key; // final
final int hash; // final,hash值已经被缓存
volatile V value; // volatile保证更新可见性
final HashEntry next; //不可变,所以增删操作只能在链表头结点处进行!!
}
为什么get不加锁却能线程安全???以下按照各种情况一条一条分析
get线程安全前提是什么?其他线程在进行增删改操作时不影响读线程。
巧妙的使用了volatile和不可变对象!!!!!
a) put操作,如果hashenrty中当前key已经存在,只是更新value为新值,由于value为volatile类型,JMM可以保证更新value操作happen-before于所有同时读的操作!
b) put操作,key不存在,需要新增节点。concurrenthashmap会直接在table对应头结点位置处新增一个节点,并指向老的头结点。此时读线程如果已经获取到了老的头结点,并不影响它继续遍历get。
c) remove操作,会先把remove掉的节点前面的链表拷贝一份并拼接到被删除节点的后续节点上。如下:
删除前:bucket->a->b->c->d->e->f
删除掉c: bucket->b->a->d->e->f
拷贝后前置节点的顺序会反过来。
此时如果有读线程读到了c节点,而另外的线程把c节点删了完全不会影响读线程继续读操作。只是读线程本次读入可能会读到已经被删除的值。
- d) clear操作, 只是把所有table的头节点置空,读线程如果读到了原来的节点,还是可以继续自己的查找。
这种get请求不加锁的方式,没有办法完全的实时性保证,被remove的节点可能会被读到(写线程删除某个节点的同时,读线程已经开始遍历)。新增的节点可能没有第一时间读到值(新增的同时,读线程已经获取到旧的头结点开始遍历查找了)。,其实这种不是特别及时的读没太大关系,因为读到的值对map本身不会造成影响,如果需要先读取值再使用该值对map进行操作,需要直接使用concurrentmap提供的复合原子操作。
get方法源码:
V get(Object key, int hash) {
if (count != 0) { // read-volatile // ①//这里可以去掉吗??不可以,count的读保证了happen-before原则
HashEntry e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null) // ② 注意这里
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
- 首先,判断count是否大于0,count为volatile,能保证判断时即使有更新操作也能及时可见。
- 如果读到的值为null,加锁重读。
为什么读到的值会可能为null?
这就是对象的安全发布问题了,hashentry除了value外都是final类型,jmm能保证他们的安全初始化,完全属于不可变对象,所以读取这些属性都不会为null,但是1.value不是final类型,2.hashentry对象在segment中并没有按照安全发布对象的方式发布, 3.concurrenthashmap本身就不支持值为null(我在想可能就是因为这里不好区分吧,如果用户本来就是传入的null呢~~~)。所以这里如果为null就是hashentry还没有构造完成(此时可能写线程正在构造中),所以想要获取真实的value值,就必须等待写线程将hashentry构造完成,所以就必须加锁等待。
V readValueUnderLock(HashEntry e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
所以,只有当get操作获取到一个并未构造完成的对象值时,才需要加锁。
size,isEmpty,contains,containsValue等需要全局查找(遍历所有segment)的操作如何实现的。
- size:执行最多RETRIES_BEFORE_LOCK次累加segment中count值操作,并同时累加并记录下来统计前每一个segment的modcount,count累加结束后判断modcount总值或者每一个segment的modcount是否有改变,如果变了则从新再来。如果RETRIES_BEFORE_LOCK次周而复始都没有得到稳定的结果,则进行全局segment加锁,并统计count操作!
因为count是volatile,所以虽然modCount不是volatile类型,但是由于count的存在,使得modCount也可见了,为什么呐?因为count肯定是遵循happen-before原则的,而map源码本身保证所有的结构更新操作都会在操作的最后一步执行更新count,所以hb(Segment内部结构更新,count更新)[单线程内的happen-before原则],然后hb(count更新,count读),按照传递性hb(Segment内部结构更新,count读),所以执行了count读后,后续对segment内部不管是modCount还是内部的bucket,都是能看到刚刚更新的新值!!!!!!!
public int size() {
final Segment[] segments = this.segments;
long sum = 0;
long check = 0;
int[] mc = new int[segments.length];
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
check = 0;
sum = 0;
int mcsum = 0;
for (int i = 0; i < segments.length; ++i) {
sum += segments[i].count;
mcsum += mc[i] = segments[i].modCount;
}
if (mcsum != 0) {
for (int i = 0; i < segments.length; ++i) {
check += segments[i].count;
if (mc[i] != segments[i].modCount) {
check = -1; // force retry
break;
}
}
}
if (check == sum)
break;
}
if (check != sum) { // Resort to locking all segments
sum = 0;
for (int i = 0; i < segments.length; ++i)
segments[i].lock();
for (int i = 0; i < segments.length; ++i)
sum += segments[i].count;
for (int i = 0; i < segments.length; ++i)
segments[i].unlock();
}
if (sum > Integer.MAX_VALUE)
return Integer.MAX_VALUE;
else
return (int)sum;
}
- containsValue
public boolean containsValue(Object value) {
if (value == null)
throw new NullPointerException();
// See explanation of modCount use above
final Segment[] segments = this.segments;
int[] mc = new int[segments.length];
// Try a few times without locking
for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
int sum = 0;
int mcsum = 0;
for (int i = 0; i < segments.length; ++i) {
int c = segments[i].count;
mcsum += mc[i] = segments[i].modCount;
if (segments[i].containsValue(value))
return true;
}
boolean cleanSweep = true;
if (mcsum != 0) {
for (int i = 0; i < segments.length; ++i) {
int c = segments[i].count; // 特别注意
if (mc[i] != segments[i].modCount) {
cleanSweep = false;
break;
}
}
}
if (cleanSweep)
return false;
}
// Resort to locking all segments
for (int i = 0; i < segments.length; ++i)
segments[i].lock();
boolean found = false;
try {
for (int i = 0; i < segments.length; ++i) {
if (segments[i].containsValue(value)) {
found = true;
break;
}
}
} finally {
for (int i = 0; i < segments.length; ++i)
segments[i].unlock();
}
return found;
}
特别注意int c = segments[i].count;
这句话,只是为了获取volatile的count以保证modCount也能获取到实时更新的值。
迭代器
和get操作一样,弱一致性迭代器,为了保证get,put等高频操作的高并发性!弱一致是指在迭代过程中如果有元素被删除了或者新增了可能不会在迭代过程中反映出来。concurrenthashmap的迭代器不会抛fast-fail异常!
总结
concurrenthashmap的高并发性支撑在:
- 锁分离,不同segment不同的锁
- 读操作不需要加锁
- size,isempty等全map遍历操作做了一定程度优化。