在Map的实现类中除了HashMap和HashTable以外还有一个ConcurrentHashMap。HashMap和HashTable一都非常熟悉,HashTable是线程安全的、且不能存储Null值,HashMap是非线程安全的、可以存储Null值。
ConcurrentHashMap是java.util.concurrent包中的类,他是结合HashMap和HashTable而来的Map实现类。从名字就知道是一种线程安全的Map类,那他和线程安全的HashTable有什么区别,往下看就知道了。
一 内部结构
在介绍ConcurrentHashMap内部结构之前,先复习下HashTable,就知道ConcurrentHashMap内部结构的设计原理了
1.1 HashTable
先看一下HashTable主要方法的方法定义:
public synchronized int size()
public synchronized boolean isEmpty()
public synchronized V get(Object key)
public synchronized V put(K key, V value)
通过上面的方法定义,可以知道HashTable之所以能够保证线程安全,是因为基本上HashTable所有的方法都通过synchronized进行了锁保护,而且是将整个HashTable对象加锁,也就是同时只能有一个线程访问、修改HashTable。这样带来的问题就是在多线程环境中代价大、效率低。
1.2 ConcurrentHashMap
HashTable效率低速度慢的原因就在于每次都要将整个HashTable锁起来,ConcurrentHashMap正是巧妙的解决了这个问题,每次只将要被修改的局部地方加上锁,其他地方还可以访问和修改。
ConcurrentHashMap的内部结构通过两个内部类实现:Segment类和HashEntry类。
每个ConcurrentHashMap对象所有数据都是存在Segment< K,V>[] segments数组里,数组的每个元素都是一个Segment对象;Segment类的主要数据结构是HashEntry< K,V>[] table数组,table数组中的HashEntry是一个链表,链表的每个结点存储一个键值对。ConcurrentHashMap中每一个键值对最终就是存储在这里。
所以说ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点)。
然后每次只对segments数组中的一个Segment对象加锁,这样segments数组中跟其他Segment对象就可以被正常访问了。所以说ConcurrentHashMap是HashMap和HashTable的结合。
ConcurrentHashMap就是通过这种局部加锁的方式提高并发访问的速度。下图就是两种Map实现类的结构对比。
二 类关系图
ConcurrentHashMap类位于java.util.concurrent包当中,实现了ConcurrentMap接口,继承了AbstractMap。
然后我们再看看ConcurrentHashMap内部类Segment的类关系图,可以发现Segment继承了Reentrantlock类,正是他完成了多线程的同步控制
三 ConcurrentHashMap类定义
我们从主要成员变量、构造函数、常用API三个方面了解ConcurrentHashMap的定义
3.1 主要成员变量
上面第一节讲了ConcurrentHashMap有两个重要的内部类Segment类和HashEntry类,下面我们就看看他们的源码
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient int count; //用于记录每个Segment桶中键值对的个数
transient int modCount; //对table的大小造成影响的操作的数量(比如put或者remove操作)
transient int threshold; //阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
transient volatile HashEntry<K,V>[] table; //链表数组,数组中的每一个元素代表了一个链表的头部
final float loadFactor; //负载因子,用于确定threshold,默认是1
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
}
可以看出来Segment是个锁对象,HashEntry是链表的一个结点,HashEntry.Key存的是每个键值对的键值,HashEntry.Value存的键值对的值。而且大多数变量都是final修饰的。
3.2 构造函数
ConcurrentHashMap一共有五个构造函数,我们重点分析下面第一个就行,其他三个都是这个构造函数实现的。最后一个构造函数相当于是复制一个新的对象。
- public ConcurrentHashMap(int ,float , int)
- public ConcurrentHashMap(int ,float)
- public ConcurrentHashMap(int)
- public ConcurrentHashMap()
- public ConcurrentHashMap(Map
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
// initialCapacity是Map中键值对初始个数
// loadFactor是负载参数
// concurrentLevel代表ConcurrentHashMap内部的Segment的数量
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; // Segment[]数组大小
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; // table[]数组大小
// 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);
this.segments = ss;
}
在初始化过程中涉及几个关键的量,我们先看构造函数的三个参数:
- initialCapacity:ConcurrentHashMap中键值对总个数,如果不指定是默认取16
- loadFactor:负载参数,用于确定table[]数组扩容阈值,就是当table[ ]数组长度大于(cap * loadFactor)时,进行扩容,未指定是默认取0.75
- concurrencyLevel:代表ConcurrentHashMap内部的segment[ ]数组的长度,concurrencyLevel在初始化后是不可改变的,也就是说segment[ ]数组长度是不可变得,所以扩容操作主要针对于table[ ]数组。这样的好处在于扩容是不需要对整个ConcurrentHashMap做rehash,只对某个table[ ]数组做rehash即可。未指定是默认取16
构造函数主要功能就是创建了sagment[ ]数组和table[ ]数组,而重点就在于计算这两个数组的大小。
- ssize:sagment[ ]数组的真实大小,sszie是不大于concurrencyLevel的最大的2的指数,他的好处在于方便采用移位操作进行hash,加快速度
- cap:table[ ]数组的大小,cap是不大于initialCapacity / ssize的最大的2的指数,好处同样也是加快hash
另外还有两个重要的变量segmentShift和segmentMask:
- segmentShift
- segmentMask
这两个量是用来定位segment桶在segments[]数组中位置用的,详见“4.1定位sagment桶”
3.3 常用API
- public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) 构造函数
- public boolean isEmpty( )
- public int size()
- public V get(Object key)
- public boolean containsKey(Object key)
- public boolean containsValue(Object value)
- public V put(K key, V value)
- public V remove(Object key)
- public void clear()
- public Set< K> keySet()
四 常用方法源码分析
4.1 定位sagment桶
在ConcurrentHashMap的增删改查操作中有一个步骤至关重要,就是要先通过key值确定键值对究竟是存在segments[ ]数组中哪个位置,table[ ]数组中哪个位置。
private Segment<K,V> segmentForHash(int h) {
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
}
参数h就是hash值,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,这里的segmentShift和segmentMask值是在构造函数中计算得到的,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。
这里有两个地方我一直还没搞懂,一是“>>> segmentShift”操作和“& segmentMask”感觉是重复的,不知道为什么要这样,多一次计算确保正确?二是后面的“<< SSHIFT) + SBASE”一直还没搞明白是在干啥。。。
计算table[]数组中的位置通过int index = (table.length - 1) & hash;实现。
4.2 put()
// ConcurrentHashMap类的put()方法
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);
// 调用Segment类的put方法
return s.put(key, hash, value, false);
}
// Segment类的put()方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 加锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 根据hash计算在table[]数组中的位置
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 判断table[]是否需要扩容,并通过rehash()函数完成扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
可以发现put的基本过程就是,通过hash值确定segments[]中segment桶的位置,然后调用Segment的put方法将键值对插入segment桶的table[]数组中,先确定table[]数组是否存在该key值和对应的位置,再插入到具体的链表位置。
有以下几点值得关注:
- 同步锁:多线程同步锁是在调用Segment类的put方式时使用的,此时其他线程不能访问当前segment桶,但是可以其他segment桶
- 扩容:在添加新的键值对之前会判断当前segment桶大小是否超过阈值threshold ,如果超过就调用rehash()进行扩容,newCapacity = oldCapacity << 1;将table[]大小扩容到原来的两倍
4.3 get( )
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
get( )方法的实现过程比较简单,首先计算hash值并确定segment桶,然后确定table[]数组中HashEntry的位置,遍历HashEntry链表,获取Value值
4.4 remove( )
// ConcurrentHashMap类的remove方法
public V remove(Object key) {
int hash = hash(key);
// 获取segment桶
Segment<K,V> s = segmentForHash(hash);
// 调用Segment类的remove方法
return s == null ? null : s.remove(key, hash, null);
}
Segment类的remove方法
final V remove(Object key, int hash, Object value) {
// 进行加锁
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
理解了put( )和get( )操作后,删除操作也就自然理解了。
之前有的版本的JDK中,HashEntry类的实现中next的声明为:final HashEntry
4.5 size( )
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
// 给所有的segment桶加锁
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
可以发现size( )方法很不一样:
- size( )方法会将整个ConcurrentHashMap遍历一次,来获得map大小
- 遍历之前会将ConcurrentHashMap中每个segment桶都加锁
五 总结
ConcurrentHashMap是结合HashTable和HashMap得到的线程安全的保证高并发的Map实现类
- ConcurrentHashMap主要结构是一个Segment[]数组,每个Segment元素都有一个HashEntry[]数组,Segment相当于一个小的HashTable
- Segment[]数组大小是不可变的,扩容操作是将Segment类中HashEntry[]扩容到原来的两倍
- put( )和remove( )方法是要加锁的,而且只对单个segment桶加锁
- size( )需要遍历整个ConcurrentHashMap,而且每个segment桶都要加锁
参考资料:
http://www.iteye.com/topic/1103980
http://www.cnblogs.com/ITtangtang/p/3948786.html