注:代码摘自jdk1.7
ConcurrentHashMap是java.util.concurrent中实现了ConcurrentMap接口的一个线程安全的HashMap,功能和hashtable类似,但是实现原理却不一样。
Hashtable原理很简单,就是利用synchronized去保证线程安全,所以并发量很低。
而ConcurrentHashMap的实现原理,就是把一个hashmap分成若干个小的hashmap,每个小的hashmap有一把锁,这样,当对不同小hashmap操作的时候,就可以并发的进行了。当然,这样做也会带来一些问题,怎么判断ConcurrentHashMap的size,是否为空等。
ConcurrentHashMap中,分成的小hashmap是用一个内部类Segment,segment继承了ReentrantLock。
static final class Segment<K,V> extends ReentrantLock implements Serializable {而整个ConcurrentHashMap的结构如下图(网上找的)
其实从这张图就大概知道ConcurrentHashMap的工作原理,无非就是put的时候,先hash一次,找出是哪个Segment,然后再在Segment中,找出是哪个桶。get和remove也类似。
但是里面有很多细节的地方。
首先看下put函数
@SuppressWarnings("unchecked")
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);
}
中间那段(有位移操作的两行)可以理解为怎么通过hash值安全的取出Segment。然后放在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;
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;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
第一行,首先是尝试获得锁tryLock(),如果没有获得,那么调用scanAndLockForPut()方法。
1.找出当前hash值对应的桶的第一个节点,然后遍历hash值和equals方法看是否存在当前key值。如果有的话,就改变对应的value值
2.如果没有找出key,则创建node,插入。
3.最后释放锁
这里的modCount是对Segment每进行一次操作(插入,删除)就会++, 而count则是记录当前Segment是否为空。一看就知道,肯定是为了size()和isEmpty服务的。
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
scanAndLockForPut函数,在一定时间范围内,先不去竞争锁(因为竞争锁就有可能阻塞,阻塞就会影响效率),在这个时间范围内,也不闲着,就看当前Segment中,有没有包含当前key。有的话,我就先创建一个node,并返回。当然,期间还要不断检测Segment是否有变化。
remove和get方法和put的思想类似,就不在说明。
在看看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) {
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;
}
重点是for循环里面,首先是不用加锁来统计size,也就是每个Segment里面的count,但是,你每次遍历的时候,可以另外的线程在写或者删元素。只要满足2次遍历,modCount没有变,那么就认为,在第二次循环的时候,没有线程在改变Segment的size。但如果在一定时间,没有通过不加锁的方式获得size,那么就只有先把每个获取每个Segment的锁,在统计size。
isEmpty的方法思想和size类似。
ConcurrentHashMap和HashMap的区别。
最直观的当然是一个线程安全一个不安全。
但是还有就是ConcurrentHashMap不运行null的key。
ConcurrentHashMap有HashMap没有的方法
//如果存在key,则返回key对应的value,如果不存在,则插入key—value
V putIfAbsent(K key, V value);
//当key和value都一样,才删除
boolean remove(Object key, Object value);
//当key和value都一样,才替换
boolean replace(K key, V oldValue, V newValue);
//把当前key的value改为value,如果不存在key返回null
<pre name="code" class="cpp">boolean replace(K key, V value);