ConcurrentHashMap分析.docx

上传人:b****3 文档编号:4919397 上传时间:2022-12-11 格式:DOCX 页数:16 大小:24.13KB
下载 相关 举报
ConcurrentHashMap分析.docx_第1页
第1页 / 共16页
ConcurrentHashMap分析.docx_第2页
第2页 / 共16页
ConcurrentHashMap分析.docx_第3页
第3页 / 共16页
ConcurrentHashMap分析.docx_第4页
第4页 / 共16页
ConcurrentHashMap分析.docx_第5页
第5页 / 共16页
点击查看更多>>
下载资源
资源描述

ConcurrentHashMap分析.docx

《ConcurrentHashMap分析.docx》由会员分享,可在线阅读,更多相关《ConcurrentHashMap分析.docx(16页珍藏版)》请在冰豆网上搜索。

ConcurrentHashMap分析.docx

ConcurrentHashMap分析

ConcurrentHashMap是Java5中引入的支持高并发、高吞吐量的线程安全HashMap实现。

在这之前我对ConcurrentHashMap只有一些肤浅的理解,仅知道它采用了多个锁,大概也足够了。

但是在经过一次惨痛的面试经历之后,我觉得必须深入研究它的实现。

面试中被问到读是否要加锁,因为读写会发生冲突,我说必须要加锁,我和面试官也因此发生了冲突,结果可想而知。

还是闲话少说,通过仔细阅读源代码,现在总算理解ConcurrentHashMap实现机制了,其实现之精巧,令人叹服,与大家共享之。

实现原理

锁分离(LockStripping)

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。

它使用了多个锁来控制对hash表的不同部分进行的修改。

ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hashtable,它们有自己的锁。

只要多个修改操作发生在不同的段上,它们就可以并发进行。

有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。

这里“按顺序”是很重要的,否则极有可能出现死锁,在ConcurrentHashMap内部,段数组是final的,并且其成员变量实际上也是final的,但是,仅仅是将数组声明为final的并不保证数组成员也是final的,这需要实现上的保证。

这可以确保不会出现死锁,因为获得锁的顺序是固定的。

不变性是多线程编程占有很重要的地位,下面还要谈到。

1.        /** 

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

3.         */  

4.        final Segment[] segments;  

 

不变(Immutable)和易变(Volatile)

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。

如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。

ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。

HashEntry代表每个hash链中的一个节点,其结构如下所示:

 static final class HashEntry {  

1.    final K key;  

2.    final int hash;  

3.    volatile V value;  

4.    final HashEntry next;  

5.}  

可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,所有的节点的修改只能从头部开始。

对于put操作,可以一律添加到Hash链的头部。

但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。

这在讲解删除操作时还会详述。

为了确保读操作能够看到最新的值,将value设置成volatile,这避免了加锁。

其它

 为了加快定位段以及段中hash槽的速度,每个段hash槽的的个数都是2^n,这使得通过位运算就可以定位段和段中hash槽的位置。

当并发级别为默认值16时,也就是段的个数,hash值的高4位决定分配在哪个段中。

但是我们也不要忘记《算法导论》给我们的教训:

hash槽的的个数不应该是2^n,这可能导致hash槽分配不均,这需要对hash值重新再hash一次。

这是重新hash的算法,还比较复杂,我也懒得去理解了。

Java代码

1.private static int hash(int h) {  

2.    // Spread bits to regularize both segment and index locations,  

3.    // using variant of single-word Wang/Jenkins hash.  

4.    h += (h <<  15) ^ 0xffffcd7d;  

5.    h ^= (h >>> 10);  

6.    h += (h <<   3);  

7.    h ^= (h >>>  6);  

8.    h += (h <<   2) + (h << 14);  

9.    return h ^ (h >>> 16);  

10.} 

这是定位段的方法:

Java代码

1.final Segment segmentFor(int hash) {  

2.    return segments[(hash >>> segmentShift) & segmentMask];  

3.}  

数据结构

关于Hash表的基础数据结构,这里不想做过多的探讨。

Hash表的一个很重要方面就是如何解决hash冲突,ConcurrentHashMap和HashMap使用相同的方式,都是将hash值相同的节点放在一个hash链中。

与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)。

下面是ConcurrentHashMap的数据成员:

Java代码

1.public class ConcurrentHashMap extends AbstractMap  

2.        implements ConcurrentMap, Serializable {  

3.    /** 

4.     * Mask value for indexing into segments. The upper bits of a 

5.     * key's hash code are used to choose the segment. 

6.     */  

7.    final int segmentMask;  

8.  

9.    /** 

10.     * Shift value for indexing within segments. 

11.     */  

12.    final int segmentShift;  

13.  

14.    /** 

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

16.     */  

17.    final Segment[] segments;  

18.} 

 

所有的成员都是final的,其中segmentMask和segmentShift主要是为了定位段,参见上面的segmentFor方法。

每个Segment相当于一个子Hash表,它的数据成员如下:

 

Java代码

1.    static final class Segment extends ReentrantLock implements Serializable {  

2.private static final long serialVersionUID = 2249069246763182397L;  

3.        /** 

4.         * The number of elements in this segment's region. 

5.         */  

6.        transient volatile int count;  

7.  

8.        /** 

9.         * Number of updates that alter the size of the table. This is 

10.         * used during bulk-read methods to make sure they see a 

11.         * consistent snapshot:

 If modCounts change during a traversal 

12.         * of segments computing size or checking containsValue, then 

13.         * we might have an inconsistent view of state so (usually) 

14.         * must retry. 

15.         */  

16.        transient int modCount;  

17.  

18.        /** 

19.         * The table is rehashed when its size exceeds this threshold. 

20.         * (The value of this field is always (int)(capacity * 

21.         * loadFactor).) 

22.         */  

23.        transient int threshold;  

24.  

25.        /** 

26.         * The per-segment table. 

27.         */  

28.        transient volatile HashEntry[] table;  

29.  

30.        /** 

31.         * The load factor for the hash table.  Even though this value 

32.         * is same for all segments, it is replicated to avoid needing 

33.         * links to outer object. 

34.         * @serial 

35.         */  

36.        final float loadFactor;  

37.} 

 

count用来统计该段数据的个数,它是volatile,它用来协调修改和读取操作,以保证读取操作能够读取到几乎最新的修改。

协调方式是这样的,每次修改操作做了结构上的改变,如增加/删除节点(修改节点的值不算结构上的改变),都要写count值,每次读取操作开始都要读取count的值。

这利用了Java5中对volatile语义的增强,对同一个volatile变量的写和读存在happens-before关系。

modCount统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变,在讲述跨段操作时会还会详述。

threashold用来表示需要进行rehash的界限值。

table数组存储段中节点,每个数组元素是个hash链,用HashEntry表示。

table也是volatile,这使得能够读取到最新的table值而不需要同步。

loadFactor表示负载因子。

 实现细节

 修改操作

 先来看下删除操作remove(key)。

Java代码

1.public V remove(Object key) {  

2. hash = hash(key.hashCode());  

3.    return segmentFor(hash).remove(key, hash, null);  

4.} 

 

整个操作是先定位到段,然后委托给段的remove操作。

当多个删除操作并发进行时,只要它们所在的段不相同,它们就可以同时进行。

下面是Segment的remove方法实现:

Java代码

1.V remove(Object key, int hash, Object value) {  

2.    lock();  

3.    try {  

4.        int c = count - 1;  

5.        HashEntry[] tab = table;  

6.        int index = hash & (tab.length - 1);  

7.        HashEntry first = tab[index];  

8.        HashEntry e = first;  

9.        while (e !

= null && (e.hash !

= hash || !

key.equals(e.key)))  

10.            e = e.next;  

11.  

12.        V oldValue = null;  

13.        if (e !

= null) {  

14.            V v = e.value;  

15.            if (value == null || value.equals(v)) {  

16.                oldValue = v;  

17.                // All entries following removed node can stay  

18.                // in list, but all preceding ones need to be  

19.                // cloned.  

20.                ++modCount;  

21.                HashEntry newFirst = e.next;  

22.                for (HashEntry p = first; p !

= e; p = p.next)  

23.                    newFirst = new HashEntry(p.key, p.hash,  

24.                                                  newFirst, p.value);  

25.                tab[index] = newFirst;  

26.                count = c; // write-volatile  

27.            }  

28.        }  

29.        return oldValue;  

30.    } finally {  

31.        unlock();  

32.    }  

33.} 

 

整个操作是在持有段锁的情况下执行的,空白行之前的行主要是定位到要删除的节点e。

接下来,如果不存在这个节点就直接返回null,否则就要将e前面的结点复制一遍,尾结点指向e的下一个结点。

e后面的结点不需要复制,它们可以重用。

下面是个示意图,我直接从这个网站上复制的(画这样的图实在是太麻烦了,如果哪位有好的画图工具,可以推荐一下)。

  删除元素之前:

 删除元素3之后:

 

第二个图其实有点问题,复制的结点中应该是值为2的结点在前面,值为1的结点在后面,也就是刚好和原来结点顺序相反,还好这不影响我们的讨论。

 

整个remove实现并不复杂,但是需要注意如下几点。

第一,当要删除的结点存在时,删除的最后一步操作要将count的值减一。

这必须是最后一步操作,否则读取操作可能看不到之前对段所做的结构性修改。

第二,remove执行的开始就将table赋给一个局部变量tab,这是因为table是volatile变量,读写volatile变量的开销很大。

编译器也不能对volatile变量的读写做任何优化,直接多次访问非volatile实例变量没有多大影响,编译器会做相应优化。

 

接下来看put操作,同样地put操作也是委托给段的put方法。

下面是段的put方法:

Java代码

1.V put(K key, int hash, V value, boolean onlyIfAbsent) {  

2.    lock();  

3.    try {  

4.        int c = count;  

5.        if (c++ > threshold) // ensure capacity  

6.            rehash();  

7.        HashEntry[] tab = table;  

8.        int index = hash & (tab.length - 1);  

9.        HashEntry first = tab[index];  

10.        HashEntry e = first;  

11.        while (e !

= null && (e.hash !

= hash || !

key.equals(e.key)))  

12.            e = e.next;  

13.  

14.        V oldValue;  

15.        if (e !

= null) {  

16.            oldValue = e.value;  

17.            if (!

onlyIfAbsent)  

18.                e.value = value;  

19.        }  

20.        else {  

21.            oldValue = null;  

22.            ++modCount;  

23.            tab[index] = new HashEntry(key, hash, first, value);  

24.            count = c; // write-volatile  

25.        }  

26.        return oldValue;  

27.    } finally {  

28.        unlock();  

29.    }  

30.} 

 

该方法也是在持有段锁的情况下执行的,首先判断是否需要rehash,需要就先rehash。

接着是找是否存在同样一个key的结点,如果存在就直接替换这个结点的值。

否则创建一个新的结点并添加到hash链的头部,这时一定要修改modCount和count的值,同样修改count的值一定要放在最后一步。

put方法调用了rehash方法,reash方法实现得也很精巧,主要利用了table的大小为2^n,这里就不介绍了。

 

修改操作还有putAll和replace。

putAll就是多次调用put方法,没什么好说的。

replace甚至不用做结构上的更改,实现要比put和delete要简单得多,理解了put和delete,理解replace就不在话下了,这里也不介绍了。

 

 获取操作

 首先看下get操作,同样ConcurrentHashMap的get操作是直接委托给Segment的get方法,直接看Segment的get方法:

 

Java代码

1.V get(Object key, int hash) {  

2.    if (count !

= 0) { // read-volatile  

3.        HashEntry e = getFirst(hash);  

4.        while (e !

= null) {  

5.            if (e.hash == hash && key.equals(e.key)) {  

6.                V v = e.value;  

7.                if (v !

= null)  

8.                    return v;  

9.                return readValueUnderLock(e); // recheck  

10.            }  

11.            e = e.next;  

12.        }  

13.    }  

14.    return null;  

15.} 

 

get操作不需要锁。

第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count变量,通过这种机制保证get操作能够得到几乎最新的结构更新。

对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。

接下来就是对hash链进行遍历找到要获取的结点,如果没有找到,直接访回null。

对hash链进行遍历不需要加锁的原因在于链指针next是final的。

但是头指针却不是final的,这是通过getFirst(hash)方法返回,也就是存在table数组中的值。

这使得getFirst(hash)可能返回过时的头结点,例如,当执行get方法时,刚执行完getFirst(hash)之后,另一个线程执行了删除操作并更新头结点,这就导致get方法中返回的头结点不是最新的。

这是可以允许,通过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。

要得到最新的数据,只有采用完全的同步。

 

最后,如果找到了所求的结点,判断它的值如果非空就直接返回,否则在有锁的状态下再读一次。

这似乎有些费解,理论上结点的值不可能为空,这是因为put的时候就进行了判断,如果为空就要抛NullPointerException。

空值的唯一源头就是HashEntry中的默认值,因为HashEntry中的value不是final的,非同步读取有可能读取到空值。

仔细看下put操作的语句:

tab[index]=newHashEntry(key,hash,first,value),在这条语句中,HashEnt

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 考试认证 > 公务员考试

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1