上篇我们讲到HashMap,从整个代码的实现上,我们看到没有任何一个关于synchronized或者Lock的字眼,所以HashMap是线程不安全的,Java提供了一个线程安全的HashMap,当然也可以通过Collections.synchronizedMap来实现Map线程安全,与List里面提到的方式一样,也是很暴力的直接给每个读取方法加一个synchronized字段,这里不再赘述。我们讲一下Java的线程安全利器ConcurrentHashMap
1、类定义
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
public ConcurrentHashMap() {
}
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;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
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;
}
}
2、增加元素
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ConcurrentHashMap 不允许key和value为null,这个hashmap是没有限制的,注意
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 计算hash值,见注1
int binCount = 0;
// 这里是一个死循环,所以一定要内部break了,才能出来
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 如果table未生成,先初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 如果对应hash值位置没有元素,则在相应位置生成一个node节点,作为链表和树的根节点,至于tabAt与casTabAt不是java层实现的,实际就是CAS的方式实现,具体见3节,而且这边的操作也是不加锁的。
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)
// 正在扩容中,则先扩容,下一次for循环进来,在添加,见注2
tab = helpTransfer(tab, f);
else {
// 其他情况,则进行hashmap中提到的链表插入或者红黑树插入
V oldVal = null;
// 进行插入操作的时候,需要进行加锁。主要是防止多个写入操作一起进行
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
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) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
注1:
spread函数
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
从hashmap计算hash余数的时候,我们怎么计算的?
hash * (capacity - 1) ,这样会带来什么问题,在capacity较小的时候,我们用到的更多的是hash值的低位信息,所以为了充分利用hash值的特征,将hash值的低16位和高16位进行异或计算,这样目的是为了减少碰撞。
注2:
helpTransfer函数
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 如果当前table的的元素正在扩容,则
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length); // 算出当前table长度的扩容标识
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) { // sizeCtl为负数时标识正在扩容
// 判断当前是否需要新建扩容线程
if ((sc >>> RESIZE_STAMP_SHIFT) != rs // 是不是与当前的扩容标识相同
|| sc == rs + 1 // 当前是不是扩容完成,因为每次新起扩容线程时,会+1,而完成一个扩容,又会-1
|| sc == rs + MAX_RESIZERS // 当前扩容线程是否达到最大
|| transferIndex <= 0)
break;
// 新建一个扩容线程,此时设置sizeCtl + 1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab); // 扩容具体操作,见章节4
break;
}
}
return nextTab;
}
return table;
}
此函数,如名字,叫帮助扩容,实际上就是帮助当前已有的扩容行为B创建扩容线程T。本身扩容行为B就是由扩容线程T组成,所以创建线程时,需要校验有扩容行为B。那rs = resizeStamp(tab.length)实际上就是这个标识,这个标识与当前的table长度一一对应,所以可以作为标识。而为了能让sizeCtl同时表示标识和当前线程数量,则首先将rs左移16位,也就是高位,而低16位则表示线程数量,所以才可以保证+1表示线程增加,-1表示线程完成。
3、CAS的原理
全称是比较和交换(Compare And Swap),这个 被广泛用在jdk8的很多多线程机制里,实际上是jvm通过c代码实现的,因为底层是通过cpu来实现的,所以不同处理器的方式有所不同。大致原理如下:
每个线程需要改变值的时候,首先会拷贝一份该值的复制,比如原来是1,那么当某一个线程需要改变值的时候,他首先会把自己工作内存的值与主内存中值,进行compare,如果发现值变更了,则认为此操作失败,重新进行操作,知道发现compare的值相同时,进行swap操作,更改主内存中的值。所以这种方式可以在大多数情况下,实现原子操作,但是也有例外,因为他比较的只是值,所以一旦出现ABA,比如一个值从A变成了B又从B变成了A,但是对于某一个线程,他去执行compare的时候,就不认为该值变化了。所以java提供了了一种AtomicStampedReference增加一个版本号进行变更,可以避免此问题。
4、扩容理解
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 根据CPU数量,计算以几个一组(桶区间)进行计算
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // 默认是16个作为一组
// 如果nextTab为空,也就是默认扩容的时候,先初始化新的table
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 新的table扩容两倍
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n; // 迁移前的table长度
}
int nextn = nextTab.length;
// 被标识为ForwardingNode的桶,认为被一个线程占用
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; // 当前的线程是否处理完成
boolean finishing = false; // 用于标识是否扩容完成
// bound 表示当前线程可以处理的当前桶区间最小下标
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) { // 4.1循环
int nextIndex, nextBound;
// 4.2默认i是大于bound的,首次基本会进入到下面4.4的逻辑,然后每处理完一个元素(桶)之后,就会再次走入4.1的循环,进行-1操作之后,如果这一组还没有处理好,就会一直满足4.2的条件,此时就会直接跳出4.1循环,进行元素的处理,一旦不满足4.2了,也就是处理完桶区间的最小下标元素了,就会走到4.3(完成扩容线程的分配)或者4.4 处理下一组的条件里
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) { // 4.3当前最大下标已经是table的长度了。表示已经完成线程分配了
i = -1;
advance = false;
}
// 4.4设置当前线程要处理的下一组bound
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1; // 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)) {
// 表示当前已经扩容完成,这个在addCount函数中有涉及(想了解的具体可参看源码),首次创建线程的时候,创建的首个线程的标识,就是通过计算 resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2得到,因为增加线程是+1,完成一个线程扩容是-1,所以当所有的都完成了,就恢复到了resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2。
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); // 如果原空间是null,则直接将该位置设置为ForwardingNode,也就是标识一下该元素被占用。
else if ((fh = f.hash) == MOVED)
advance = true; // 表示已经完成了扩容
else {
// 锁元素(桶),也就是在真正进行扩容操作的时候,才对元素加锁。而不是暴力的对整个hashmap对象上锁,效率很高。
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;
}
}
}
}
}
}