从ConcurrentHashMap能学到哪些并发编程技巧?

2019-05-18 20:50:35

阅读次数 - 527

Java 并发 无锁编程

前两天写了一篇文章《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点了,在自己写并发代码的时候可以参考借鉴这些思路。

感谢阅读


觉得文章还不错?点击下方按钮分享吧!

评论

快来占个沙发吧~


TO: