Java并发包concurrent------ConcurrentHashMap

ConcurrentHashMap从JDK1.5开始开始随java.util.concurrent包一起引入JDK中,主要解决HashMap线程不安全和Hashtable效率不高的问题。HashMap是线程不安全的,而Hashtable中的方法加上了synchronized关键字而线程安全了,但是效率低下,所以ConcurrentHashMap的出现解决了这个问题,ConcurrentHashMap既线程安全又效率高。

在JDK1.7之前的ConcurrentHashMap是使用分段锁机制实现,在JDK1.8中则是使用数组+链表+红黑树的数据结构和CAS原子操作实现ConcurrentHashMap,下面将两种实现方式一一介绍:

1.ConcurrentHashMap的实现---JDK1.7版本

1.1分段锁机制

HashTable效率低下是因为对put操作加了锁synchronized,而synchronized是对整个对象进行加锁,也就是说在执行put操作时锁住了整个hash表,使得多线程时其实是串行执行,效率低下。因此在JDK1.5~1.7使用了分段锁机制实ConcurrentHashMap。

简单来说,就是ConcurrentHashMap在对象中保存了一个Segment数组,即将整个hash表划分为多个分段;每一个segment,即每一个分段类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此ConcurrentHashMap在多线程并发编程时可以实现多线程put操作。下面将详细分析其原理:

1.2ConcurrentHashMap的数据结构

ConcurrentHashMap类结构如上图所示,在ConcurrentHashMap定义中,定义了一个Segment<K, V>[]数组来将Hash表实现分段存储,从而实现分段加锁;而么一个Segment元素则与HashMap结构类似,其包含了一个HashEntry数组,用来存储Key/Value对。Segment继承了ReetrantLock,表示Segment是一个可重入锁,因此ConcurrentHashMap通过可重入锁对每个分段进行加锁。

1.3ConcurrentHashMap的初始化

JDK1.7的ConcurrentHashMap初始化主要分为两个部分:一是初始化ConcurrentHashMap,即初始化segments数组,segmentShift段偏移量和segmentMask段掩码等;然后则是初始化每个segment分段。接下来,将介绍这两部分的初始化:

ConcurrentHashMap包含多个构造函数,而所有的构造函数最终都调用了如下的构造函数:

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

由代码可知,该构造函数传入了三个参数:initialCapacity、loadFactor、concurrencyLevel,其中,concurrencyLevel主要用来初始化segments,segmentShift和segmentMask等;而initialCapacity和loadFactor则主要用来初始化每个Segment分段。

1.3.1初始化ConcurrentHashMap

根据ConcurrentHashMap的构造方法可知,在初始化时创建了两个中间变量ssize和sshift,它们都是通过concurrentLevel计算得到的。其中ssize表示了segments数组的长度,为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方,所以在初始化时通过循环计算出一个大于或等于concurrencyLevel的最小的2的N次方值来作为数组的长度;而sshift表示了计算ssize时进行移位操作的次数。

segmentShift用于定位参与散列运算的位数,其等于32减去sshift,使用32是因为ConcurrentHashMap的hash()方法返回的最大数是32位的;segmentMask是散列运算的掩码,等于ssize减去1,所以掩码的二进制各位都为1。

因为ssize的最大长度为65536,所以segmentShift最大值为16,segmentMask最大值为65535. 由于segmentShift和segmentMask与散列运算相关,因此之后还会对此进行分析。

1.3.2 初始化Segment分段

ConcurrentHashMap通过initialCapacity和loadFactor来初始化每个Segment. 在初始化Segment时,也定义了一个中间变量cap,其等于initialCapacity除以ssize的倍数c,如果c大于1,则取大于等于c的2的N次方,cap表示Segment中HashEntry数组的长度;loadFactor表示了Segment的加载因子,通过cap*loadFactor获得每个Segment的阈值threshold。

默认情况下,initialCapacity等于16,loadFactor等于0.75,concurrencyLevel等于16。

1.4定位Segment

由于采用了Segment分段锁机制实现一个高效的同步,那么首先则需要通过hash散列计算key的hash值,从而定位其所在的segment,因此来看看ConcurrentHashMap中的hash()方法的实现:

    private int hash(Object k) {
        int h = hashSeed;
 
        if ((0 != h) && (k instanceof String)) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
 
        h ^= k.hashCode();
 
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

通过hash函数可知,首先通过计算一个随机的hashSeed减少String类型的key值的hash冲突;然后利用Wang/Jenkins hash算法对key的hash值进行再hash计算。通过这两种方式都是为了减少散列冲突,从而提高效率。因为如果散列的质量太差,元素分布不均,那么使用Segment分段加锁也就没有意义了。
 

    private Segment<K,V> segmentForHash(int h) {
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
    }

接下来,ConcurrentHashMap就能通过上述定位函数定位到key所在的segment分段。

1.5ConcurrentHashMap的操作

在介绍ConcurrentHashMap的操作之前,首先需要介绍一下Unsafe类,因为在JDK1.7新版本中是通过Unsafe类的方法实现锁操作的。Unsafe类是一个保护类,一般应用程序很少用到,但其在一些框架中经常用到,如JDK、Netty、Spring等框架。Unsafe类提供了一些硬件级别的原子操作,其在JDK1.7和JDK1.8中的ConcurrentHashMap都有用到,但其用法却不同,在此只介绍在JDK1.7中用到的几个方法:

  • arrayBaseOffset(Class class):获取数组第一个元素的偏移地址。
  • arrayIndexScale(Class class):获取数组中元素的增量地址。
  • getObjectVolatile(Object obj, long offset):获取obj对象中offset偏移地址对应的Object型field属性值,支持Volatile读内存语义。

1.5.1get

JDK1.7的ConcurrentHashMap的get操作是不加锁的,因为在每个Segment中定义的HashEntry数组和在每个HashEntry中定义的value和next HashEntry节点都是volatile类型的,volatile类型的变量可以保证其在多线程之间的可见性,因此可以被多个线程同时读,从而不用加锁。而其get操作步骤也比较简单,定位Segment –> 定位HashEntry –> 通过getObjectVolatile()方法获取指定偏移量上的HashEntry –> 通过循环遍历链表获取对应值。

  • 定位Segment:(((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE
  • 定位HashEntry:(((tab.length - 1) & h)) << TSHIFT) + TBASE

1.5.2put

    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

put方法首先也会通过hash算法定位到对应的Segment,此时,如果获取到的Segment为空,则调用ensureSegment()方法;否则,直接调用查询到的Segment的put方法插入值,注意此处并没有用getObjectVolatile()方法读,而是在ensureSegment()中再用volatile读操作,这样可以在查询segments不为空的时候避免使用volatile读,提高效率。在ensureSegment()方法中,首先使用getObjectVolatile()读取对应Segment,如果还是为空,则以segments[0]为原型创建一个Segment对象,并将这个对象设置为对应的Segment值并返回。

在Segment的put方法中,首先需要调用tryLock()方法获取锁,然后通过hash算法定位到对应的HashEntry,然后遍历整个链表,如果查到key值,则直接插入元素即可;而如果没有查询到对应的key,则需要调用rehash()方法对Segment中保存的table进行扩容,扩容为原来的2倍,并在扩容之后插入对应的元素。插入一个key/value对后,需要将统计Segment中元素个数的count属性加1。最后,插入成功之后,需要使用unLock()释放锁。

1.5.3size

ConcurrentHashMap的size操作的实现方法也非常巧妙,一开始并不对Segment加锁,而是直接尝试将所有的Segment元素中的count相加,这样执行两次,然后将两次的结果对比,如果两次结果相等则直接返回;而如果两次结果不同,则再将所有Segment加锁,然后再执行统计得到对应的size值。

2.ConcurrentHashMap的实现------JDK8版本

JDK1.7之前的分段锁机制,最大并发度受Segment的个数限制,JDK1.8中,加锁则采用CAS和synchronized实现。

2.1CAS原理

一般地,锁分为悲观锁和乐观锁:悲观锁认为对于同一个数据的并发操作,一定是为发生修改的;而乐观锁则认为对于同一个数据的并发操作不会发生修改,在更新数据时会采用尝试更新不断重试的方式更新数据。

CAS(Compare And Swap):CAS有三个操作数,内存值V,预期值A,要修改的新值B,当且仅当A和V相等时才会修改为B,否则什么都不做。Java中CAS操作通过本地JNI本地方法实现,在JVM中程序会根据当前处理器的类型决定是否为cmpxchg指令添加前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock cmpxchg);反之,如果程序运行在单处理器上,就省略lock前缀。

CAS同时具有volatile读和volatile写的内存语义。

CAS存在的问题:存在ABA问题,解决思路:使用版本号;循环时间长,开销大;只能保证一个共享变量的原子操作。

2.2ConcurrentHashMap的数据结构

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值