从ConcurrentHashMap能学到哪些并发编程技巧?
2019-05-18 20:50:35
阅读次数 - 815
前两天写了一篇文章《ConcurrentHashMap的put源码解析》。昨天晚上与RedSpider技术社区的小伙伴一起把ConcurrentHashMap
的其它主要方法(get、remove、初始化、扩容等)也过了一遍。
其实我们学习源码不能仅仅只是为了面试或了解这个类的运行机制。更应该从别人的代码里面学习到一些有用的编程思想。
ConcurrentHashMap是Doug Lea所写。它在JDK 1.7中用的是“分段锁”的思想,但在JDK 1.8中几乎重写了一遍,把数据结构简化成与HashMap类似,减小了锁的粒度,大道至简,进一步提升了它在多线程下的性能。
这篇文章主要总结一下在ConcurrentHashMap源码学习过程中学到的一些并发编程的思想和技巧。
1 使用volatile进行通信
在ConcurrentHashMap中有一个很重要的属性是sizeCtl
它被用于初始化和扩容的过程中。且不同阶段它的值有不同的含义。比如在扩容阶段,它是一个负数,绝对值就代表了参与扩容的线程的数量。它是一个volatile
变量,定义如下:
private transient volatile int sizeCtl;
在初始化或扩容等过程中,多个线程都会用到这个变量,它本身是volatile的,所以可以保证线程间的可见性。除此之外,多线程之间没有使用其它复杂的通信方式。
2 使用CAS避免锁
在之前的那篇put源码解析的文章我们可以看到,put过程中,如果判断数组的某个位置没有结点的话,就会使用casTabAt
方法来存放新的结点。这个方法底层是使用的Unsafe
类的compareAndSetObject
方法,是一个native的CAS方法。
// put过程
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
// casTabAt方法,底层使用CAS
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSetObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
关于CAS的原理和使用,可以参考之前我写过的这篇文章:《CAS与原子操作》。CAS也是一种“无锁编程”的思想。在可预测的较小的粒度下,可以使用CAS代替锁来提升性能。
3 尽量减小锁的粒度
在面试中,可能经常会问到这个问题:HashMap, HashTable, ConcurrentHashMap有什么区别?稍微有点经验的人都能回答:“HashMap是线程不安全的,另外两个都是线程安全的,其中HashTable是在每个方法上使用了synchronized
关键字,而ConcurrentHashMap使用了更小粒度的锁,ConcurrentHashMap在并发场景下性能更好。”
是的,ConcurrentHashMap为了提升性能,在JDK 1.7中使用了“分段锁”的概念,其核心思想是只锁住某一段,这样就不会影响其他段的数据,其它线程就可以并行地操作其它段的数据。“段”虽然相较于HashTable的整个实体来说,锁粒度有所降低,但仍然是一个较粗的粒度。
JDK 1.8对此作了进一步的改进,取消的“分段锁”的概念,直接以某个桶的头结点(如果是链表,就是首结点;如果是红黑树,就是根节点)来作为锁,使锁的粒度更低,能够更大程度地支持并发。
synchronized (f) {
// ...
}
4 让空闲线程参与工作
这是我觉得ConcurrentHashMap里面非常棒的一个思想。在Map进行扩容的时候,会锁住所有put、remove等操作。这个时候如果有其它线程进行put、remove操作,它不会阻塞在那里傻傻的等待。而是会先检查是否正在进行扩容,如果是,就参与进去帮助扩容。
与其让你闲着,不如来帮我先把扩容做完吧!
那这多个线程是怎么配合工作的呢?——仍然是上面提到的以头结点为锁,再配合自旋+CAS进行轻量级通信。
主要就是这4点了,在自己写并发代码的时候可以参考借鉴这些思路。
感谢阅读
觉得文章还不错?点击下方按钮分享吧!
评论
快来占个沙发吧~