ConcurrentHashMap1.7源码分析

HashMap不是线程安全的,其所有的方法都未同步,虽然可以使用Collections的synchronizedMap方法使其线程安全,但是针对的只是当前的map对象。

对此,JDK提供了线程安全的Hashtable,其所有的方法都是同步的,使用的是全局同步锁,即使用Synchronized关键字进行加锁,但是也导致了同一时刻只能有一个线程操作哈希表,影响性能;

 

所以,又提供了效率较高的线程安全的哈希表ConcurrentHashMap,其内部使用分段锁将锁进行细粒度化,里面包含一个Segment数组,Segment继承于ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力及指向下一个节点的指针。

实际上每个Segment都是一个HashMap,默认的Segment的个数是16,也就是支持16个线程的并发读写。每个Segment内部互不干扰,从而使多个线程能够同时操作哈希表,大大提高了性能。

不同Segment的写入可以并发执行,同一个Segment的写和读可以并发执行。

下面是其内部结构示意图:

 

Like {@link Hashtable} but unlike {@link HashMap}, this class does <em>not</em> allow {@code null} to be used as a key or value    和Hashtable一样,不允许null键和null值

 

Concurrent1.7和1.8的区别:

1.7中采用Segment+HashEntry的方式进行实现,并发控制使用ReentrantLock来实现。

  每一个Segment元素存储的是HashEntry数组+链表,和HashMap的数据结构相同

 1.8中放弃了Segment臃肿的设计,取而代之的是采用Node数组+链表+红黑树的数据结构来实现,当链表的长度大于8时会转化为红黑树。

  并发控制使用 Synchronized+CAS来保证

 

  JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考

JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的(logn),代替一定阈值的链表,这样形成一个最佳拍档


JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock:
  1、因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,

    更加的灵活,而在低粒度中,Condition的优势就没有了
  2、JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
  3、在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

  原文:https://blog.csdn.net/qq296398300/article/details/79074239

以下源码基于Java 1.7版本

public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
        implements ConcurrentMap<K, V>, Serializable {
    private static final long serialVersionUID = 7249069246763182397L;

其成员变量:

  /**
     * The maximum number of segments to allow; used to bound
     * constructor arguments. Must be power of two less than 1 << 24.
   * 分段锁的最大数量
*/ static final int MAX_SEGMENTS = 1 << 16; /** * Number of unsynchronized retries in size and containsValue * methods before resorting to locking. This is used to avoid * unbounded retries if tables undergo continuous modification * which would make it impossible to obtain an accurate result.
   * 分段锁的最小数量
*/ static final int RETRIES_BEFORE_LOCK = 2; /** * Mask value for indexing into segments. The upper bits of a * key's hash code are used to choose the segment.
* 分段锁的掩码值
*/ final int segmentMask; /** * Shift value for indexing within segments.
* 分段锁的移位值
*/ final int segmentShift; /** * The segments, each of which is a specialized hash table.
* 分段锁数组
*/ final Segment<K,V>[] segments;

Segment是ConcurrentHashpMap的一个静态内部类:

 static final class Segment<K,V> extends ReentrantLock implements Serializable {
 static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
 transient volatile HashEntry<K,V>[] table;
 transient int count;
 transient int modCount;

 transient int threshold;
 final float loadFactor;

Segment继承了ReentrantLock ,所以其本质上是一个锁。

 分段锁即是由Segment来实现的,它继承于ReentrantLock,用来管理它辖区内的各个HashEntry。ConcurrentHashMap被Segment分成了很多小区,Segment就相当于小区保安,

HashEntry列表相当于小区业主,小区保安通过加锁的方式,保证每个Segment内都不发生冲突。

put和remove方法,对于Segment内部元素和计数器的更新,全部处于锁的保护下,如Segment.put()方法的第一行:

HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);

 

 参考:https://www.cnblogs.com/liuyun1995/p/8631264.html

 

END.


posted @ 2019-06-05 11:11  杨岂  阅读(461)  评论(0编辑  收藏  举报