在Java 1.5之前,如果需要可以在多线程和并发的程序中安全使用的Map,只能在HashTable和Collections.synchronizedMap中选择,因为它们的put、reomve和containsKey方法都是同步的。我们熟知的HashMap不是线程安全的,因此在多线程环境下开发不能用这个。
HashTable容器使用synchronized来保证线程安全,因此读和写都是串行的,在线程竞争激烈的情况 下HashTable的效率非常低下。
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把 锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是 ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据 的时候,其他段的数据也能被其他线程访问。
简单的说,Hashtable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是 一次锁住一个桶(表中的一段)。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常用操作只锁当前需要用到的桶。 这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。
上面说到的16个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在size等操作时才需要锁住整个hash表。
效率提升了,但也存在弊端:
由于一些更新操作,如put(),remove(),putAll(),clear()只锁住操作的部分,所以在检索操作不能保证返回的是最新的结果。
在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
ConcurrentMap接口定义如下:
public interface ConcurrentMap<K, V> extends Map<K, V> {
V putIfAbsent(K key, V value);
boolean remove(Object key, Object value);
boolean replace(K key, V oldValue, V newValue);
V replace(K key, V value);
}
里面定义了几个基于 CAS(Compare and Set)原子操作,使用起来很方便
putifAbsent()方法
很多时候我们希望在元素不存在时插入元素,我们一般会像下面那样写代码
private final Map<String, Long> map = new Hashmap<>();
public long get(String key) {
if (map.get(key) == null){
return map.put(key, getvalue());
} else{
return map.get(key);
}
}
上面这段代码在单线程开发中是好用的,但在多线程中是有出错的风险的。这是因为在put操作时并没有对整个Map加锁,所以一个线程正在put(k,v)的时候,另一个线程调用get(k)会得到null,这就会造成一个线程put的值会被另一个线程put的值所覆盖。当然,我们可以将代码封装到synchronized代码块中,这样虽然线程安全了,但会使你的代码变成了单线程。
ConcurrentHashMap提供的putIfAbsent(key,value)原子方法的实现了同样的功能,同时避免了上面的线程竞争的风险。
private final Map<String, Long> map = new ConcurrentHashMap<>();
public long get(String key) {
Long val =map.get(key);
if(val == null){
val = getvalue();
Long l = map.putIfAbsent(key, val);
//l != null说明有别的线程捷足先登插入了key-value
if (l != null) {
val=l;
}
}
return val;
}
特别注意: putIfAbsent 方法是有返回值的,并且返回值很重要。如果(调用该方法时)key-value 已经存在,则返回那个 value 值。如果调用时 map 里没有找到 key 的 mapping,就插入新的元素并返回一个 null 值。所以,使用 putIfAbsent 方法时切记要对返回值进行判断。
Replace()方法
举个例子:统计文本中单词出现的次数,把单词出现的次数记录到一个Map中,代码如下:
private final Map<String, Long> wordCounts = new ConcurrentHashMap<>();
public long increase(String word) {
Long oldValue = wordCounts.get(word);
if(oldValue == null) {
wordCounts.put(word, 1L);
}
else{
wordCounts.put(word, oldValue + 1);
}
return newValue;
}
如果多个线程并发调用这个increase()方法,就会出现问题,因为在wordCounts.put时,其它线程已经改写了wordCounts.put的条件。比如在 当前线程执行wordCounts.get(word)之后和wordCounts.put(word, 1L);语句之前,另一个线程进行了wordCounts.put操作,当前线程再执行下去就会put错误的值。
下面用原子操作Replace()方法解决这个问题:
private final ConcurrentMap<String, Long> wordCounts = new ConcurrentHashMap<>();
public long increase(String word) {
Long oldValue, newValue;
while (true) {
oldValue = wordCounts.get(word);
if (oldValue == null) {
// Add the word firstly, initial the value as 1
newValue = 1L;
if (wordCounts.putIfAbsent(word, newValue) == null) {
break;
}
} else {
newValue = oldValue + 1;
if (wordCounts.replace(word, oldValue, newValue)) {
break;
}
}
}
return newValue;
}
用while (true) 是因为原子操作有失败的可能,因此需要多次尝试,直到成功。