JDK源码-java7-ConcurrentHashMap探讨

时间:2023-01-01 19:16:25

本文目录

开篇明志

利用本文对java1.7中的 ConcurrentHashMap 进行简短的探讨和总结。另外一篇关于Java1.8-ConcurrentHashMap的文章大家可以参考——《JDK源码-java8-ConcurrentHashMap的实现原理与应用》


简述实现原理

HashTable是一个线程安全的类,它使用synchronized来锁住整张Hash表来实现线程安全,即每次锁住整张表让线程独占。ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的Hashtable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。通过把整个Map分为N个Segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍,默认提升16倍【不扩容情况下16倍】

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。这可以确保不会出现死锁,因为获得锁的顺序是固定的。


内部结构图

ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图:
JDK源码-java7-ConcurrentHashMap探讨
如图所示,Java1.7中的ConcurrentHashMap内部分为多个Segments,而Segment继承自ReentrantLock【重入锁】

    /**
相当于多个HashTable
* Segments are specialized versions of hash tables. This
* subclasses from ReentrantLock opportunistically, just to
* simplify some locking and avoid separate construction.
*/

static final class Segment<K,V> extends ReentrantLock implements Serializable{

}

Segment继承了ReentrantLock,每一个Segment都拥有自己本段范围内的一个锁。只有需要对全局进行操作时候才会用到锁住所有segment,比如之前提到的size() 和 containsValue()操作, 此时需要按顺序锁定说有端,操作完毕后,又按顺序释放所有段的锁,按顺序可以避免死锁。


重要实体类讲解

ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应上面的图可以看出之间的关系

在ConcurrentHashMap内部,段数组是final声明的,并且其成员变量实际上也是final的,下面是的不变性声明。

    //这段代码位于 java1.7 ConcurrentHashMap 源码中 类成员变量部分

/* ---------------- Fields -------------- */

/**
* The segments, each of which is a specialized hash table.
*/

final Segment<K,V>[] segments;

ConcurrentHashMap允许多个读操作并发进行,读操作并不需要加锁, 它还保证HashEntry几乎是不可变得。HashEntry代表每个Hash链中的一个节点,源码如下:

    //此处我们仅关注HashEntry的类成员变量 及其声明特点
/**
* ConcurrentHashMap list entry. Note that this is never exported
* out as a user-visible Map.Entry.
*/

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;
}
//此处省略若干行代码......

可以看到除了value不是final的,其他都是final声明的,这意味着不能从hash链的中间或者尾部添加或删除节点,因为这需要修改next引用值,因此,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除节点的下一个节点。为了确保读操作能够看到最新的值,将value设置成volatile,这样避免加锁。