【翻译】Java
ConcurrentHashMap
的最佳实践
ConcurrentHashMap
的最佳实践
ConcurrentHashMap
除了提供了与
HashMap
类似的功能外,内部实现了同步机制。这意味着,我们当我们需要在多线程应用中使用
ConcurrentHashMap
时不再需要使用
synchronized
块。
//Initialize ConcurrentHashMap instance
ConcurrentHashMap<String, Integer> m = new ConcurrentHashMap<>();
m.put("id-1", 1);
m.put("id-2", 2);
m.put("id-3", 3);
//Print all values stored in ConcurrentHashMap instance
for(Entry<String,Integer> e : m.entrySet()){
System.out.println(threadName + ": " + e.getKey() + " = " + e.getValue());
}
上述代码确保了应该在多线程环境下运行的合理有效。为什么用合理有效这个词来描述呢,因为上述代码兼顾了线程安全和代码的运行效率。ConcurrentHashMap
的引入,就是在确保线程安全的同时改进代码运行的效率。那么,ConcurrentHashMap
是如何做到的呢?
要解释这个问题,我们就需要理解ConcurrentHashMap
类的内部工作原理。首先,我们来看看ConcurrentHashMap
类构造函数的参数列表。参数最多的构造函数需要传入三个参数:
1. initialCapacity
2. loadFactor
3. concurrencyLevel
从命名上来看,前面两个参数非常容易理解,但最后一个参数名字就比较晦涩。最后一个参数代表了数据的片段数量。这个数量用来将ConcurrentHashMap
数据内部划分为多个部分,同时创建相同数量的线程对ConcurrentHashMap
数据进行维护。
ConcurrentHashMap
concurrencyLevel
缺省值为16,也就是说,当我们使用ConcurrentHashMap
的默认构造函数创建一个实例时,在添加第一个键值对前,ConcurrentHashMap
就拥有了16个数据片段。意味着,创建了16个各内部类的实例,如:ConcurrentHashMap$Segment
、ConcurrentHashMap$HashEntry[]
以及ReentrantLock$NonfairSync
。
一般应用程序中的多数场景,用多线程处理单个键值对数据量合理的数据片段时,性能表现并不差。使用多个数据片段会让内部处理变得时常复杂,并且还会为垃圾回收带来大量不必要的对象,这些对程序性能都会有影响。
使用默认构造函数来创建ConcurrentHashMap
一个对象会额外创建1%至50%的其它对象。因此,100个这样的ConcurrentHashMap
对象就会有5000个额外创建的对象。
基于以上的分析,我们建议使用合理的构造函数参数,来减少不必要的对象数量,以提高性能。
一个好的初始化方法是如下形式:
ConcurrentHashMap<String, Integer> instance = new ConcurrentHashMap<>(16,0.9f,1);
容量参数16来减少或避免Map
的扩容。负载因子0.9保证ConcurrentHashMap
的数据空集程序,以优化内存的使用。concurrentLevel
设置为1确保内部只需要创建和维护一个数据片段。
需要注意的是,如果开发的是一个高并发应用,并且对ConcurrentHashMap
的更新频率非常高,我们就需要考虑使用大于1的值来设置concurrentLevel
,但这个数字需要分析我们的应用场景来设计。