Java多线程/并发14、保持线程间的数据独立:ConcurrentHashMap应用

来源:互联网 发布:我心伤悲 莫知我哀评价 编辑:程序博客网 时间:2024/05/21 02:19

在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) 是因为原子操作有失败的可能,因此需要多次尝试,直到成功。

0 0
原创粉丝点击