concurrentHashMap并发容器简介:
concurrentHashMap代替同步Map(Collection.synchronized(new HashMap())因为HashMap是根据散列值分段存储的,同步Map在同步的时候会锁住所有的段,而conCurrentHashMap加锁的时候根据散列值锁住散列值对应的那段(segment)提高了并发性能。
conCurrentHashMap与HashMap、HashTable对比
1、HashMap是非线程安全的,
2、HashTable是线程安全的,HashTable采用的是synchronized进行同步,相当于所有线程进行读写时去竞争一把锁,导致效率低下。
3、conCurrentHashMap读操作不需要加锁,写操作只需要根据散列值所在的segment进行加锁,不需要对整个conCurrentHashMap加锁.
conCurrentHashMap内部结构
ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组。
Segment结构
static final class Segment extends ReentrantLock implements Serializable {
transient volatile int count;---Segment中元素的数量
transient int modCount;--对table的大小造成影响的操作的数量(如put或者remove)
transient int threshold;---阈值,Segment元素的数量超过这个值会对Segment进行扩容
transient volatile HashEntry[] table;--链表数组,数组中的每元素代表一个链表头部
final float loadFactor;--负载因子,用于确定threshold
}
Segment中的元素是以HashEntry的形式存放在链表数组中的
static final class HashEntry {
final K key;
final int hash;
volatile V value;
final HashEntry next; //链表
}
除了value都是final的,目的是防止链表结构被破坏,出现concurrentModification的情况。
ConcurrentHashMap是线程安全的HashMap的实现,默认构造同样有initialCapacity和loadFactor属性,不过还多了一个concurrencyLevel属性,三属性默认值分别为16、0.75及16。其内部使用锁分段技术,维持这锁Segment的数组,在Segment数组中又存放着Entity[]数组,内部hash算法将数据较均匀分布在不同锁中。
concurrentLevel:代表concurrentHashMap内部的Segment数量,concurrentLevel一经指定,不可改变,后续如果concurrentHashMap元素数量增加导致扩容。Segment的数量为2的指数:方便采用移位操作进行hash,加快hash过程。每一个segment的容量大小为2的指数:加快hash过程
扩容策略:不会增加segment的数量,而是增加segment中链表数组的容量大小,好处:扩容不需对整个concurrentHashMap做rehash,而只需要对segment中的元素做一次rehash。
操作:
(1)定位元素:定位需要两次hash,第一次hash定位到相应segment块,第二次hash定位到元素所在的链表的头部。
(2)put操作:并没有在此方法上加上synchronized,首先对key.hashcode进行hash操作,得到key的hash值。hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),接着调用此Segment对象的put方法来完成当前操作。
ConcurrentHashMap基于concurrencyLevel划分出了多个Segment来对key-value进行存储,从而避免每次put操作都得锁住整个数组。在默认的情况下,最佳情况下可允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。
(3)get(key)
首先对key.hashCode进行hash操作,基于其值找到对应的Segment对象,调用其get方法完成当前操作。而Segment的get操作首先通过hash值和对象数组大小减1的值进行按位与操作来获取数组上对应位置的HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的HashEntry产生不一致性,那么ConcurrentHashMap是如何保证的?
对象数组大小的改变只有在put操作时有可能发生,由于HashEntry对象数组对应的变量是volatile类型的,因此可以保证如HashEntry对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了HashEntry对象后,怎么能保证它及其next属性构成的链表上的对象不会改变呢?这点ConcurrentHashMap采用了一个简单的方式,即HashEntry对象中的hash、key、next属性都是final的,这也就意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry对象后,其基于next属性构建的链表是不会发生变化的。
(4)Remove
hash找到segment,锁住lock此segment进行,再次hash确定链表头部,遍历找到要删除的元素,找到以后将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去。(不能单纯的指向next,因为HashEntry中的next是final,不允许改变)
总结:
ConcurrentHashMap默认情况下采用将数据分为16个段进行存储,并且16个段分别持有各自不同的锁Segment,锁仅用于put和remove等改变集合对象的操作,基于volatile及HashEntry链表的不变性实现了读取的不加锁。这些方式使得ConcurrentHashMap能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的Map而言,而它采用的这些方法也可谓是对于Java内存模型、并发机制深刻掌握的体现。