你真会使用ConcurrentHashMap吗?—Concurr
前言
刷过一些面试题或者做过初级开发的都知道,ConcurrentHashMap是线程安全的HashMap,那么它的线程安全到底指的是什么?是只要使用了ConcurrentHashMap,对它的所有操作都是线程安全的了吗?这很容易让我们初学者对ConcurrentHashMap的使用产生误解。
理解ConcurrentHashMap的线程安全
实际上ConcurrentHashMap确实已经足够线程安全了,哈哈哈!在使用ConcurrentHashMap进行写数据时,使用segment 类(继承了ReentrantLock)来加锁处理,这样我们完全不需要担心多线程并发的情况下底层链表数组会被破坏的情况(链接丢失或构成循环)。
但是,如果加上我们自己的垃圾代码,这个完美的线程安全立刻就可以被破解!来看看下面的代码,需求是统计ArraysList中每个单词出现的次数:
List<String> words = new ArrayList<>(20);
words.add("......");//省略添加单词
Map<String,Long> map =new ConcurrentHashMap<>();
for (String word: words){
Long oldValue = map.get(word);
Long newValue = oldValue==null?1:oldValue+1;
map.put(word,newValue);
}
显然,上面的代码不是线程安全的,不能保证数据的原子性。这就需要我们今天要讲的重点:ConcurrentHashMap如何进行原子更新。
ConcurrentHashMap原子更新
原始的原子更新做法:
do
{
oldValue = map.get(word);
newValue = oldValue ==null?1:oldValue+1;
}while (!map.replace(word,oldValue,newValue));
这种方法原子类的实现是一个道理,感兴趣可以看看https://www.jianshu.com/p/a8918fac2c6c。但这样是不是太麻烦了?在Java SE 8中,我们可以这样写:
map.putIfAbsent(word,new LongAdder());//Absent是缺席的意思,就是如果map中不存在就初始化一个LongAdder进去,LongAdder()就是上面说的原子类,保证下面加一操作的原子性
map.get(word).increment();//获取word加一
另外:Java SE8还提供了一些可以更方便地完成原子更新的方法。调用compute方法时可以提供一个键和一个计算新值的函数。这个函数接受键和相关联的值(如果没有值,则为null),它会计算新值。例如,可以如下更新一个整数计数器的映射:
map.compute(word,(k,v)->v==null?1:1+v);
我们还可以这样写:
map.computeIfAbsent(word,s->new LongAdder()).increment();
这与putIfAbsent方法几乎一模一样,不过使用computeIfAbsent时,LongAdder构造器只在确实需要一个新的计数器时才会调用。
最后,再不厌其烦地介绍一个方法:哈哈!
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
我们在首次增加一个键时通常需要做些特殊的处理。利用merge方法可以非常方便地做到这一点。这个方法第二个参数表示键不存在时使用的初始值。否则就会调用你提供的函数来结合原值和初始值。所有上面的代码我们还可以这么写:
map.merge(word,1L,(exisitingValue,newValue)->existingValue+newValue);
或者,再简化下:
map.merge(word,1L,Long::sum);
警告
最后说明一下,在使用compute或merge时,如果传入的函数返回Null,将会删除map中现有的条目。且函数不能做太多的工作,因为这个函数运行时,肯能会阻塞对映射的其他更新。