ConcurrentHashMap
用于实现更好的多线程解决方案。那么我什么时候应该使用ConcurrentSkipListMap
呢?这是一种多余的做法吗?这两种数据结构的多线程方面是否相同?
ConcurrentHashMap
用于实现更好的多线程解决方案。那么我什么时候应该使用ConcurrentSkipListMap
呢?这是一种多余的做法吗?这两个类在某些方面有所不同。
ConcurrentHashMap在其合同中未保证其操作的运行时间。同时,它允许为特定负载因子(大致上是同时修改它的线程数)进行调整。
另一方面,ConcurrentSkipListMap保证广泛操作的平均O(log(n))性能。它也不支持为了并发而进行调整。 ConcurrentSkipListMap
还有一些ConcurrentHashMap
没有的操作:ceilingEntry/Key、floorEntry/Key等。它还维护一个排序顺序,如果你使用ConcurrentHashMap
,则需要计算(付出显著的代价)。
基本上,为不同的用例提供了不同的实现。 如果您需要快速添加单个键/值对和快速查找单个键,则使用HashMap
。 如果您需要更快的有序遍历,并且可以承担插入的额外成本,则使用SkipListMap
。
*虽然我预计实现大致符合一般哈希映射的保证:O(1)插入/查找;忽略重新哈希。
有关数据结构的定义,请参见跳表。
ConcurrentSkipListMap
按其键的自然顺序(或您定义的其他键顺序)存储 Map
。因此,它的 get
/put
/contains
操作速度比 HashMap
慢,但为了弥补这一点,它支持 SortedMap
、NavigableMap
和 ConcurrentNavigableMap
接口。
那么什么时候应该使用ConcurrentSkipListMap?
当你需要保持键按顺序排列和/或需要可导航映射的第一个/最后一个,头/尾和子映射功能时。
ConcurrentHashMap
类实现了ConcurrentMap
接口,ConcurrentSkipListMap
也实现了这个接口。但是如果你还想要SortedMap
和NavigableMap
的行为,则应该使用ConcurrentSkipListMap
。
ConcurrentHashMap
ConcurrentSkipListMap
以下表格指导您了解Java 11中捆绑的各种Map
实现的主要特点。点击/轻触可放大。
请注意,您可以从其他来源(如Google Guava)获取其他Map
实现和类似的数据结构。
String
相关的类和接口。 - Basil Bourque就性能而言,当skipList
用作Map时,似乎要慢10-20倍。以下是我的测试结果(Java 1.8.0_102-b14,win x32):
Benchmark Mode Cnt Score Error Units
MyBenchmark.hasMap_get avgt 5 0.015 ? 0.001 s/op
MyBenchmark.hashMap_put avgt 5 0.029 ? 0.004 s/op
MyBenchmark.skipListMap_get avgt 5 0.312 ? 0.014 s/op
MyBenchmark.skipList_put avgt 5 0.351 ? 0.007 s/op
此外,比较两个集合的用例也很重要。使用这两种集合来实现最近最少使用(LRU)缓存的功能。现在跳表的效率看起来更加可疑。
MyBenchmark.hashMap_put1000_lru avgt 5 0.032 ? 0.001 s/op
MyBenchmark.skipListMap_put1000_lru avgt 5 3.332 ? 0.124 s/op
这里是 JMH 的代码(作为 java -jar target/benchmarks.jar -bm avgt -f 1 -wi 5 -i 5 -t 1
执行)
static final int nCycles = 50000;
static final int nRep = 10;
static final int dataSize = nCycles / 4;
static final List<String> data = new ArrayList<>(nCycles);
static final Map<String,String> hmap4get = new ConcurrentHashMap<>(3000, 0.5f, 10);
static final Map<String,String> smap4get = new ConcurrentSkipListMap<>();
static {
// prepare data
List<String> values = new ArrayList<>(dataSize);
for( int i = 0; i < dataSize; i++ ) {
values.add(UUID.randomUUID().toString());
}
// rehash data for all cycles
for( int i = 0; i < nCycles; i++ ) {
data.add(values.get((int)(Math.random() * dataSize)));
}
// rehash data for all cycles
for( int i = 0; i < dataSize; i++ ) {
String value = data.get((int)(Math.random() * dataSize));
hmap4get.put(value, value);
smap4get.put(value, value);
}
}
@Benchmark
public void skipList_put() {
for( int n = 0; n < nRep; n++ ) {
Map<String,String> map = new ConcurrentSkipListMap<>();
for( int i = 0; i < nCycles; i++ ) {
String key = data.get(i);
map.put(key, key);
}
}
}
@Benchmark
public void skipListMap_get() {
for( int n = 0; n < nRep; n++ ) {
for( int i = 0; i < nCycles; i++ ) {
String key = data.get(i);
smap4get.get(key);
}
}
}
@Benchmark
public void hashMap_put() {
for( int n = 0; n < nRep; n++ ) {
Map<String,String> map = new ConcurrentHashMap<>(3000, 0.5f, 10);
for( int i = 0; i < nCycles; i++ ) {
String key = data.get(i);
map.put(key, key);
}
}
}
@Benchmark
public void hasMap_get() {
for( int n = 0; n < nRep; n++ ) {
for( int i = 0; i < nCycles; i++ ) {
String key = data.get(i);
hmap4get.get(key);
}
}
}
@Benchmark
public void skipListMap_put1000_lru() {
int sizeLimit = 1000;
for( int n = 0; n < nRep; n++ ) {
ConcurrentSkipListMap<String,String> map = new ConcurrentSkipListMap<>();
for( int i = 0; i < nCycles; i++ ) {
String key = data.get(i);
String oldValue = map.put(key, key);
if( (oldValue == null) && map.size() > sizeLimit ) {
// not real lru, but i care only about performance here
map.remove(map.firstKey());
}
}
}
}
@Benchmark
public void hashMap_put1000_lru() {
int sizeLimit = 1000;
Queue<String> lru = new ArrayBlockingQueue<>(sizeLimit + 50);
for( int n = 0; n < nRep; n++ ) {
Map<String,String> map = new ConcurrentHashMap<>(3000, 0.5f, 10);
lru.clear();
for( int i = 0; i < nCycles; i++ ) {
String key = data.get(i);
String oldValue = map.put(key, key);
if( (oldValue == null) && lru.size() > sizeLimit ) {
map.remove(lru.poll());
lru.add(key);
}
}
}
}
-tc 1,2,4,8
来查看在 1、2、4、8 线程下的性能),这将更加有趣。我预计并发哈希映射表(即使使用默认配置)将击败并发跳表映射表,但垂直可扩展性曲线可能会很有趣。 - AlecConcurrentHashMap:当您需要多线程索引式的get/put时,只支持索引式操作。Get/Put的时间复杂度为O(1)。
ConcurrentSkipListMap:除了get/put之外,还有更多的操作,例如按键排序的前/后n个项目、获取最后一个条目、按键排序获取/遍历整个映射等。时间复杂度为O(log(n)),因此put性能不如ConcurrentHashMap。它是使用SkipList实现的ConcurrentNavigableMap。
总之,当您需要在地图上执行更多需要排序功能的操作而不仅仅是简单的get和put时,请使用ConcurrentSkipListMap。