源码解读
1.7底层实现原理
Segment对象
继承ReentrantLock重入锁
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
默认的构造函数
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//initialCapacity=16,loadFactor=0.75 ,concurrencyLevel=16
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//concurrencyLevel 默认值是16 相当于16把锁
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
//segments数组的容量
int ssize = 1;
//循环完成之后 sshift=4 ssize=16
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//28 计算index右移动28位
this.segmentShift = 32 - sshift;
//与运算的时候均匀的存放到每个segment对象
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//表示HashEntry<K,V>[] table初始容量
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
//默认为2
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
//创建一个s0对象,目的是方便后期其他key落到不同的segment中,能够创建segment对象的加载因子,初始化容量的大小
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
//创建segment数组容量 默认容量大小为16
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
put方法
public V put(K key, V value) {
Segment<K,V> s;
//空的value就报错
if (value == null)
throw new NullPointerException();
//计算hash值
int hash = hash(key);
//segmentShift=28 segmentMask=15,初始化的时候设置的,计算在segments数组中的索引下标位置
int j = (hash >>> segmentShift) & segmentMask;
//获取对应的segments
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
//把对应的key和value放到对应的segemnts下面的HashEntry数组中
return s.put(key, hash, value, false);
}
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
//如果对应的索引下标的segment对象为空
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
//则拿到s0模板参数
Segment<K,V> proto = ss[0];
//segement对象中的HashEntry数组的容量
int cap = proto.table.length;
//加载因子
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
//创建hashEntry对象
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
//再次校验是否为空,如果不为空则创建新的segement对象
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
//最后一次校验是否存在,如果不存在则把上面创建的segemnt对象放到segments数组中
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//获取对应的segment对应的锁 如果获取都为true,node为空
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
//计算在HashEntry数组中的索引下标位置
int index = (tab.length - 1) & hash;
//获取对应索引下标位置的值
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
//对应索引下标的值不为空
if (e != null) {
K k;
//如果key相同则修改
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
//如果之前不存在该key,并且已经遍历到链表的最后一个节点
else {
//node不为空则采用头插法直接赋值
if (node != null)
node.setNext(first);
else
//node为空 则创建新的HashEntry对象,并且采用的是头插法
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//是否HashEntry数组需要扩容,注意Segments数组一旦初始化完成之后就不能修改了,不支持扩容,只是segment对象中的HashEntry对象支持扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
//比如first=a ,然后线程1要进行赋值b修改,线程2要加进行赋值c,刚好在同一个segment对象下面HashEntry数组中的索引位置是2
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//对应索引下标位置,如index=2,first=a
HashEntry<K,V> first = entryForHash(this, hash);
//e=a
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1;
//不断的循环获取锁
while (!tryLock()) {
HashEntry<K,V> f;
//先进行遍历index=2的链表,
if (retries < 0) {
if (e == null) {
if (node == null)
//遍历链表到最后节点之后进行创建新的节点
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
//如果是同一个key则设置retry次数
else if (key.equals(e.key))
retries = 0;
else
//遍历链表
e = e.next;
}
//MAX_SCAN_RETRIES最大值是64 如果超过64则该线程进入阻塞状态,不进行自旋去获取锁
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
//如果能进来,说明现在first节点跟之前缓存的不一样,之前缓存的是a,有线程把first节点修改了,现在线程1把它改成b,b→a,所以需要重新赋值头节点和修改retries次数
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
总结
- Segments数组一旦被初始化之后就不支持扩容,是每个segment中的HashEntry数组存在扩容的情况。
- 分段锁有多个segment对象组成,每个segment对象继承ReentrantLock,使用了lock锁
- 使用了cas做修改
1.8底层原理
- 去除Segment分段锁
- 去除lock锁,采用了synchronized(jdk1.6之后进行了优化支持自旋以及锁的升级)+cas进行保证并发下的线程同步
- 数据结构是数组+链表+红黑树
- 对index下标对应的node节点上锁
- 锁的竞争,多个线程put key的时候多个key的index都相同的时候,落入到同一个node节点的时候,需要做锁的竞争,如果index不同的时候,落入到不同的node节点的时候,不需要做锁的竞争。
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//sizeCtl默认值是0,用来控制table的初始化和扩容操作,如果为-1表示有一个线程在扩容,-2表示两个线程在扩容
if ((sc = sizeCtl) < 0)
//放弃该线程的优先执行权
Thread.yield();
//使用cas进行自旋
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
//初始化node数组的大小 默认为16
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不支持key和value为空
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
//如果大于8链表就转红黑树
int binCount = 0;
//自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//初始化为空,则初始化数组
if (tab == null || (n = tab.length) == 0)
//初始化node数组
tab = initTable();
//计算对应的index下标位置
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//没有发生index冲突,使用cas去修改,如果修改成功则退出循环,如果没有修改成功说明有线程同时在该index进行修改,继续进行自旋
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 {
//如果当前索引下标发生index冲突了,则使用synchronized上锁
V oldVal = null;
//f为node节点
synchronized (f) {
//判断头节点有没有发生改变
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//判断key有没有一样
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;
}
}
}
}
//判断当前节点的链表长度是否大于8 如果大于8就转红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//计算节点的数量
addCount(1L, binCount);
return null;
}
private final void addCount(long x, int check) {
//采用了CounterCell来缓存各个node节点的数量,最后在统一算整个节点的数量,这样能够减少在并发情况下,对size++进行不断的自旋减少开销
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
CounterCell对象的作用
- 在并发情况下,如果要统计整个ConcurrentHashMap的节点的数量,对size++采用cas不断的自旋操作的时候会占用大量的cpu
- 通过使用CounterCell对象来记录每个node节点的数量,最后再统一遍历计算整个ConcurrentHashMap的数量
JDK1.8去除分段锁的原因
- 因为jdk1.7的分段锁要计算两次index值,第一次计算在segement数组中的索引位置,第二次计算在segement对象中的HashEntry中数组的位置
- 分段锁一旦初始化之后锁的数量就不会发生改变了,jdk1.8随着node数组的扩大,锁的数量也会增加
- jdk1.8只需要计算一次index值,在node数组中的哪个索引位置即可
ConcurrentHashMap使用synchronized锁的作用而不是lock锁的原因
- synchronized锁在jdk1.6之后进行了优化,支持自旋以及锁的升级过程(偏向锁、轻量级锁、重量级锁)
- lock锁的自旋过程是不支持自旋的,需要写大量的代码进行维护
jdk1.8锁的总结
- node节点没有发生index冲突的时候采用cas锁
- node节点发生index冲突采用synchronized锁
参考:蚂蚁课堂