ConcurrentHashMap与HashMap在实现上差距不大,最主要的区别在于ConcurrentHashMap采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段中的桶,从而提高并发度,并发度就是Segment的个数。
segment的定位是按照偏移量来定位的,底层使用了Unsafe类的相关方法。
HashEntry
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
Segment
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;
}
// 默认的并发级别是16,创建16个segment
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
每个Segment维护了一个count变量来统计该Segment中键值对的个数。
/**
* The number of elements. Accessed only either within locks
* or among other volatile reads that maintain visibility.
*/
transient int count;
在执行size操作的时候,需要遍历所有Segment然后把count累计起来
ConcurrentHashMap在执行size操作时先尝试不加锁,如果连续两次尝试不加锁操作得到的结果一致,就认为这个结果是正确的。否则的话,就需要对每个Segment执行加锁,然后执行size操作。
这里加锁操作主要是因为ConcurrentHashMap是并发操作,你在获取size的同时可能还在插入元素,会导致获取到的结果出现问题。第一种方法是计算前后两次获取到的值是否相等,相等的话,表明没有元素插入,结果准确。否则一旦超过三次表明结果不准备需要给当前的segment加锁执行size操作。
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
// 如果retries==2,执行++之后为3,此时加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
// sum size置0操作,主要是为了验证前后是否等。
// last是不清零的,所以只要两次对等,表明没有数据插入。
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
} // 表名没有数据插入,直接break,可以返回size
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
put操作中涉及到两个参数segmentShift\segmentMask
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
这两个参数定义在ConcurrentHashMap构造器中:
下面大致构造即使构造出segmentShift的大小以及segmentMask大小。至于初始的c
这个主要是用来辅助构建有多少个Segment的。从下面可以看到MIN_SEGMENT_TABLE_CAPACITY=2,这个为2,表示最少每个Segment中的数组需要由两个坑位。至于cap与c之间的关系,可以看到,如果c为3的话此时cap需要从2翻倍到4,ssize是segment的个数。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
// 从这里可以看出ssize=2^sshift次方
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}// 这里主要是为了获取取多少位的问题!
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0] 其余的延迟初始化
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
put操作:
int j = (hash >>> segmentShift) & segmentMask;
// 这一句操作其实看完上面的也很好理解了,为什么需要左移segmentShift位呢
// 因为你的segmentMask是根据ssize-1来的,而根据ssize = 2^sshift可以知道
// sshift比ssize少32-sshift位(segmentShift)
所以的话此时如果左移segmentShift位的话,再与的话正好取到高位相与的值就是下标
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 求下标
int j = (hash >>> segmentShift) & segmentMask;
// 如果偏移量不能定位的话,调用ensureSegment来定位
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
put真正的实现,实质是在Segment中完成put操作的,因为分段锁的缘故。
put操作主要是因为加了锁!tryLock()方法一旦获取成功则意味着lock(),而scanAndLockForPut明确了最后推出只能lock()之后break退出。无论是否找到,出口只有lock();break;return node;
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试是否能够拿到锁,拿到则取null值,否则通过scanAndLockForPut自旋获取node
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// Segment对应的HashEntry
HashEntry<K,V>[] tab = table;
// 获取在第几个hashEntry中
int index = (tab.length - 1) & hash;
// 获取到tab的第一个值
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k; // 如果找到了的话,此时替换掉值即可
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
// 管你有没有,我都直接覆盖
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
} // 没找到则一直遍历下去
e = e.next;
} // 如果遇到了tab中为null的元素。
else { // 看看node是否为null,不为null的话,表明已经新建了节点
// 只需要把自己的next设置为first头即可。
if (node != null) // 因为first==null
node.setNext(first);
else
// 相当于直接将node这个节点作为当前HashEntry的第一个节点,下一个节点为null(first)
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1; // count是记录元素个数的
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node); // 元素个数过多执行扩容
else // 一切顺利,将此node放入到当前的HashEntry中
setEntryAt(tab, index, node);
++modCount; // 修改次数++
count = c; // 元素个数为c
oldValue = null; // 老的值记录为null,因为需要返回
break;
}
}
} finally { // 释放锁
unlock();
}
return oldValue;
}
scanAndLockForPut其中使用自旋获取锁。这一段的逻辑操作为:首先获取当前key-hash=hash(key)对应的节点链的头节点,然后持续遍历该链,如果节点中不存在该节点,则预创建一个新的节点,retries=0,意味着增加1,直到retries操作达到了最大值,进入阻塞等待状态,终止!否则的话在自旋过程中如果发现头节点发生了变化,此时头节点也相应的跟随变化,并将retries置为-1,意味着从头开始。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) { // 自旋获取锁
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) { // 预创建节点
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
} // 表示找到了,retries置为0,下一轮必结束,break,返回node即可
else if (key.equals(e.key))
retries = 0;
else
e = e.next; // 即不为空也不等,证明在其中则一直下去。
}// 尝试次数如果大于1了 采用阻塞锁获取
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 && // 只要链表头不改变,执行下去,否则同步链表头,重新再来
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
rehash
操作的话是对Segment中存储元素的扩容,也就是对table的扩容。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
// 直接扩容一倍
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
// 直接构建新的table,这是为了使得并发get拿到值是没有问题的
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
// 使用新的掩码
int sizeMask = newCapacity - 1;
// 由于是2的倍数,基本上之前的位置上的索引hash值是不会变的
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i]; // 取值
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e; // 如果是一个节点的话直接存,这是Doug Lea大神的一个优化点
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;// 如果是多个节点的话
// 表示在当前槽位上
int lastIdx = idx;
// 遍历找到可重用的那个点,一般来说也不会发生变化。除非还有新的操作
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}// lastRun之后的左右的节点都是可重用的。如果一开始就没变的话那就直接重用
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
// 保证所有不可重用点都是new出来的,不会对原有结构进行改变
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
get操作比较简单,由于在存入或者put操作的时候都是新创建一个新的表,所以获取只需要定位然后根据key值以及hash值判断即可,相等就取出。
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) { // 定位segment,然后获取到HashEntry的位置
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
final V remove(Object key, int hash, Object value) {
if (!tryLock()) // 加锁失败,则采取自旋获取锁lock()
scanAndLock(key, hash);
V oldValue = null; // 移除之后需要返回的值
try {
// 获取到当前segment的table
HashEntry<K,V>[] tab = table;
// 获取当前删除元素的下标位置
int index = (tab.length - 1) & hash;
// 找到当前下标的头节点
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;// 记录前一个节点
while (e != null) {
K k; // 三个节点 pred 当前节点 next
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value; // hash与key都相等,取出值value判断是否相等
if (value == null || value == v || value.equals(v)) {
if (pred == null)// 如果pred为null,正好是首节点,就将next设置为首节点
setEntryAt(tab, index, next);
else // 否则直接跨过当前节点即可
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
} // 不然大家一起前进啊
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
ConcurrentHashMap第一个的区别就是在于使用了Node来代替Segment存储数据,使用Node代替了HashEntry
static class Node<K,V> implements Map.Entry<K,V> {
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;
}
}
其中几个比较重要的方法:后面用得着
// 获取当前数组以及指定下标的节点的原子操作
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);
}
// 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);
}
// 在某个下标处设置值的原子操作
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
其中有个比较中要的参数:
/**
* 用来控制初始化和扩容的,默认值为0,初始化的时候如果制定了大小,则这个值会保存在sizeCtl中
* 大小为数组长度的0.75,当为负的时候,说明表正在初始化或在扩张,
* -1 表示初始化
* -(1+n):其中n表示正在活动的扩张线程
*/
private transient volatile int sizeCtl;
构造方法:空构造就不看了
// 实例化的时候如果指定了容量的话,此处就将sizeCtl=cap
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;
}
初始化方法:初始化table,使用sizeCtl中记录的大小。初始化方法主要用于第一次put操作时使用
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// 小于0的话表示别的线程正在初始化,自己进行线程礼让
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 否则的话比较 SIZECTL:表示当前对象的内存偏移量
// 期望值sc,符合就将sc替换为-1
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// 指定了大小的话就按照指定大小来,否则默认16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// n-n/4=3n/4
sc = n - (n >>> 2);
}
} finally {
// 长度为3/4,即0.75
sizeCtl = sc;
}
break;
}
}
return tab;
}
传入key-value,实际调用putVal方法执行
public V put(K key, V value) {
// 第三个参数onlyIfAbscent: false的话表示value一定会被设置
// true:表示value为null时才会被设置
return putVal(key, value, false);
}
putVal()
方法,从第一句if (key == null || value == null) throw new NullPointerException();
可以看出,ConcurrentHashMap键值都不能为空。
总结put操作的过程:
首先判断键值是否为null,不为null获取当前key的hash值,获取当前table,如果当前tab没有被初始化,则先调用initTable()方法先初始化表(顺便初始化sizeCtl);已经初始化的话,此时判断当前位置的元素是否为null,是的话,利用CAS操作以key创建新节点Node插入当前位置即可。否则的话,判断当前的hash值是否为-1,看看是否有其他线程在执行扩容的复制过程,是的话自己要去帮忙,然后再回来处理put过程。都不是的话,此时可以知道当前位置是存在元素的,此时利用synchronized关键字加锁,由于允许并发操作,为防止之前取出的头节点发生变化,再次取出比较,如果还相等,证明当前Node没有插入新的元素,否则表明已经插入,直接返回null即可。
如果头节点没有发生变化,判断当前的hash值是否大于0,(-2是树化的hash值),此时就遍历Node,binCount=1,依次累加,如果找到了元素,直接覆盖。如果之前树化了,则利用putTreeVal的方法添加节点。最后判断bitCount的数量是否超过了树化的门限,超过则调用treeifyBin方法判断是扩容还是树化。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 获取key的hash值
int hash = spread(key.hashCode());
// 记录当前桶的元素个数(可以理解为坑位)
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果tab没有初始化,则执行initTable()方法初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 否则获取到当下下标处的Node节点,如果为null的话
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 通过CAS将当前新创建的节点放入即可
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
// 跳出循环,结束put操作
break; // no lock when adding to empty bin
} // MOVED:-1 用于转发节点的hash
// 如果是MOVED状态的话,表示正在进行数组扩张的数据复制阶段。
// 当前线程也会参与复制,通过允许多线程复制的功能,以此来减少数组的复制所带来的性能损失。
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // fh如果在这里没有命中,往后走只有>=0 以及 -2可取
else {
// 如果当前位置有元素的话,采用synchronized加锁
V oldVal = null;
synchronized (f) {
// 再次取出值与之前取出的对比
if (tabAt(tab, i) == f) {
// 取出的hash值大于0,如果转化为树则为-2
if (fh >= 0) {// 桶个数置为1,表示从1开始
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果找到了,则直接值覆盖,put成功
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
// 否则将之前的节点定为pred,看看后面是否为null
Node<K,V> pred = e;
// 是的话,直接创建新的Node节点拼接在pred.next处
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}// 小于0的话。表示已经树化,此时判断是否为树的类型
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// 调用putTreeVal方法,存入值
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}// binCount发生变化之后,判断是否大于树化的门限值8,是的话,树化。
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
} // binCount执行+1操作,返回null
addCount(1L, binCount);
return null;
}
Replaces all linked nodes in bin at given index unless table is too small, in which case resizes instead.
将其中的给定了index的节点替换为树中节点(链表转为树),超过64就要转,除非表太小,这种情况就参与扩容操作。
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {// 如果表不为null且大小小于64,执行扩容操作。
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1); // 直接扩成2倍即可
// 否则的话取出当前index下的Node节点并在hash值大于0的情况下
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
// 使用synchronized加锁,取出再次比较
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
// 只有一个节点的话,则head=p,当前节点为头结点
if ((p.prev = tl) == null)
hd = p;
else // 否则将其放在数的最后一个节点next处
tl.next = p;
tl = p; // tl变到下面。
}// 将当前表放入TreeBin中
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
扩容实现方法:tryPresize
private final void tryPresize(int size) {
// 如果size大于1 << 30的一半,则直接使用最大的。否则的话调用tableSizeFor来扩容
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1); // 无论怎么扩都要是2的整次幂
int sc;
// 假如之前是16的容量需要扩到32,那么sizeCtl就需要从12扩到24
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// 如果没有被初始化,则此时创建一个n大小为sc与c中的更大值的Node[]
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
// 比较置为-1.
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {// 确认其他线程没有对此表进行修改
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
// 完成之后还是设置为3/4大小
sc = n - (n >>> 2);
}
} finally {// 3/4大小
sizeCtl = sc;
}
}
}// 扩容到容量小于等于sizeCtl大小,或者n大于最大容量,终止扩容
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
// 否则获取到tab
else if (tab == table) {
int rs = resizeStamp(n);
// 如果也在初始化扩容的话,则帮助扩容,否则开始新的扩容
if (sc < 0) {
Node<K,V>[] nt;
// 第一个判断是判断右移这么多位之后与之前的n是否是同一个容量下进行扩容
// 第二个和第三个判断是判断当前扩容数是否到达最大的限制
// 第四和第五个判断是确保transfer()方法初始化完毕
// 如果都不符合,直接判定break;退出扩容
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
// 线程数+1,sc在扩容的时候表示transfer的工作线程数
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);
}
}
}
这一节的代码解释可以参看:并发编程——ConcurrentHashMap#transfer() 扩容逐行分析
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 设定线程处理桶的个数NCPU获取的CPU核心数
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {// 初始化 扩容两倍
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;// 占位节点
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 为true表示可以推进
boolean advance = true;
// 保证完成状态
boolean finishing = false; // to ensure sweep before committing nextTab
// transferIndex为转移时的下标,扩容前的tab的length
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {// 向前推进
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
get操作和jdk1.7基本一样。
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 &&
// 拿到当前的Node节点
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果当前key的哈希值与当前获取到的节点哈希值相等
if ((eh = e.hash) == h) { // 判断值是否相等,相等的话就返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0) // 如果小于0的话,此时遍历寻找,不存在返回null
return (p = e.find(h, key)) != null ? p.val : null;
// 既不等于头结点的hash值,也不小于0,遍历查找
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}// 没找到返回null
return null;
}
ConcurrentHashMap支持并发操作,从get方法中我们可以看出,没有任何任何的同步机制,所以读操作支持并发操作。
写操作的话一般是put操作,放入元素时发生写的操作,这个时候会进行相应的加锁过程(jdk1.7),在jdk1.8中主要是利用synchronized以及cas+Unsafe底层的操作来实现的同步操作。
多个线程同步处理的过程通过synchronized和unsafe两种方式来实现的。
参考