当写入操作多于读取操作时,获取并发哈希集的最佳方法是什么?
Whats the best way to obtain a concurrent hash set when write operations are excess than read operations?
我发现我们可以使用 newKeySet();
或 keySet(default value)
从 ConcurrentHashMap
获得并发哈希集。当写入操作多于读取操作时,这是创建线程安全集的最佳方法吗?
我读到 CopyOnWriteArraySet
似乎阅读比写作更好。
欢迎所有可能有助于我们对此了解更多的答案。
如果您的密钥是小整数,您可以使用位掩码。但是,如果没有更多信息,我会建议。
Set<Type> set = Collections.newSetFromMap(new ConcurrentHashMap<>());
ConcurrentHashMap.newKeySet()
和ConcurrentHashMap.keySet()
那个return一个KeySetView
依赖ConcurrentHashMap
class只在写上加锁文中关注的键位映射,并非全图。
因此,读取操作很快但写入也很快(事实上稍微慢一点)。
依赖于CopyOnWriteArrayList
的CopyOnWriteArraySet
不锁定读操作,而是锁定写操作。因此,为了保证并发读取的一致性,每个写入操作都会触发底层数组的副本。
因此,在不小的集合(例如 100
元素或更多元素)的情况下,CopyOnWriteArraySet
写操作应该比 [=13= 更昂贵(锁定整个集合 + 底层数组的副本) ](仅锁定相关条目)。
CopyOnWriteArraySet
javadoc 强调了这一点:
It is best suited for applications in which set sizes generally stay small, read-only operations vastly outnumber mutative operations, and
you need to prevent interference among threads during traversal.
Mutative operations (add, set, remove, etc.) are expensive since they usually entail copying the entire underlying array
这是我们比较两种行为的基准。
Set<String>
在每次迭代时使用 100 个元素(“0”到 99”值)进行初始化。
前6个方法是写入操作(删除,添加新元素,覆盖现有元素)。
而接下来的 4 个方法是读取操作(迭代、包含)。
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Threads(8)
public class SetBenchmark {
private Set<String> keySetView;
private Set<String> copyOnWriteArraySet;
private Random random;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(SetBenchmark.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
@Setup(Level.Iteration)
public void doSetup() {
random = new Random(1);
keySetView = ConcurrentHashMap.newKeySet();
copyOnWriteArraySet = new CopyOnWriteArraySet<>();
init(keySetView);
init(copyOnWriteArraySet);
}
private void init(Set<String> set) {
IntStream.range(0, 100)
.forEach(i -> {
final String string = String.valueOf((char) i);
set.add(string);
});
}
// Writing
@Benchmark
public void _1_keySetView_remove() {
doRemove(keySetView);
}
@Benchmark
public void _2_copyOnWriteArraySet_remove() {
doRemove(copyOnWriteArraySet);
}
@Benchmark
public void _3_keySetView_add_with_new_value() {
doAddWithNewValue(keySetView);
}
@Benchmark
public void _4_copyOnWriteArraySet_add_with_new_value() {
doAddWithNewValue(copyOnWriteArraySet);
}
@Benchmark
public void _5_keySetView_add_with_existing_value() {
doAddWithExistingValue(keySetView);
}
@Benchmark
public void _6_copyOnWriteArraySet_add_with_existing_value() {
doAddWithExistingValue(copyOnWriteArraySet);
}
// Reading
@Benchmark
public void _7_keySetView_iterate() {
String res = doIterate(keySetView);
}
@Benchmark
public void _8_copyOnWriteArraySet_iterate() {
String res = doIterate(copyOnWriteArraySet);
}
@Benchmark
public void _9_keySetView_contains() {
boolean res = doContains(keySetView);
}
@Benchmark
public void _010_copyOnWriteArraySet_contains() {
boolean res = doContains(copyOnWriteArraySet);
}
// Writing
private void doRemove(Set<String> set) {
set.remove(getRandomString());
}
private void doAddWithNewValue(Set<String> set) {
set.add(getRandomString() + set.size());
}
private void doAddWithExistingValue(Set<String> set) {
set.add(getRandomString());
}
// Reading
private String doIterate(Set<String> set) {
String result = "";
for (String string : set) {
result += string;
}
return result;
}
private boolean doContains(Set<String> set) {
return set.contains(getRandomString());
}
// Random value with seed
private String getRandomString() {
return String.valueOf(random.nextInt(100));
}
}
结果出来了(分数越低越好)。
写入操作:
Benchmark Mode Cnt Score Error Units
SetBenchmark._1_keySetView_remove avgt 61,659 ns/op
SetBenchmark._2_copyOnWriteArraySet_remove avgt 249,976 ns/op
SetBenchmark._3_keySetView_add_with_new_value avgt 240,589 ns/op
SetBenchmark._4_copyOnWriteArraySet_add_with_new_value avgt 30691,318 ns/op
SetBenchmark._5_keySetView_add_with_existing_value avgt 84,472 ns/op
SetBenchmark._6_copyOnWriteArraySet_add_with_existing_value avgt 473,592 ns/op
读取操作:
Benchmark Mode Cnt Score Error Units
SetBenchmark._7_keySetView_iterate avgt 13603,012 ns/op
SetBenchmark._8_copyOnWriteArraySet_iterate avgt 13626,146 ns/op
SetBenchmark._9_keySetView_contains avgt 53,081 ns/op
SetBenchmark._10_copyOnWriteArraySet_contains avgt 250,401 ns/op
只有迭代器的两个 Set
实现的读取速度一样快。
在任何情况下 CopyOnWriteArraySet
的写入都慢得多,但是当我们 add()
一个不存在的值时,情况更糟。
我发现我们可以使用 newKeySet();
或 keySet(default value)
从 ConcurrentHashMap
获得并发哈希集。当写入操作多于读取操作时,这是创建线程安全集的最佳方法吗?
我读到 CopyOnWriteArraySet
似乎阅读比写作更好。
欢迎所有可能有助于我们对此了解更多的答案。
如果您的密钥是小整数,您可以使用位掩码。但是,如果没有更多信息,我会建议。
Set<Type> set = Collections.newSetFromMap(new ConcurrentHashMap<>());
ConcurrentHashMap.newKeySet()
和ConcurrentHashMap.keySet()
那个return一个KeySetView
依赖ConcurrentHashMap
class只在写上加锁文中关注的键位映射,并非全图。
因此,读取操作很快但写入也很快(事实上稍微慢一点)。
CopyOnWriteArrayList
的CopyOnWriteArraySet
不锁定读操作,而是锁定写操作。因此,为了保证并发读取的一致性,每个写入操作都会触发底层数组的副本。
因此,在不小的集合(例如 100
元素或更多元素)的情况下,CopyOnWriteArraySet
写操作应该比 [=13= 更昂贵(锁定整个集合 + 底层数组的副本) ](仅锁定相关条目)。
CopyOnWriteArraySet
javadoc 强调了这一点:
It is best suited for applications in which set sizes generally stay small, read-only operations vastly outnumber mutative operations, and you need to prevent interference among threads during traversal.
Mutative operations (add, set, remove, etc.) are expensive since they usually entail copying the entire underlying array
这是我们比较两种行为的基准。
Set<String>
在每次迭代时使用 100 个元素(“0”到 99”值)进行初始化。
前6个方法是写入操作(删除,添加新元素,覆盖现有元素)。
而接下来的 4 个方法是读取操作(迭代、包含)。
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Threads(8)
public class SetBenchmark {
private Set<String> keySetView;
private Set<String> copyOnWriteArraySet;
private Random random;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(SetBenchmark.class.getSimpleName())
.warmupIterations(5)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
@Setup(Level.Iteration)
public void doSetup() {
random = new Random(1);
keySetView = ConcurrentHashMap.newKeySet();
copyOnWriteArraySet = new CopyOnWriteArraySet<>();
init(keySetView);
init(copyOnWriteArraySet);
}
private void init(Set<String> set) {
IntStream.range(0, 100)
.forEach(i -> {
final String string = String.valueOf((char) i);
set.add(string);
});
}
// Writing
@Benchmark
public void _1_keySetView_remove() {
doRemove(keySetView);
}
@Benchmark
public void _2_copyOnWriteArraySet_remove() {
doRemove(copyOnWriteArraySet);
}
@Benchmark
public void _3_keySetView_add_with_new_value() {
doAddWithNewValue(keySetView);
}
@Benchmark
public void _4_copyOnWriteArraySet_add_with_new_value() {
doAddWithNewValue(copyOnWriteArraySet);
}
@Benchmark
public void _5_keySetView_add_with_existing_value() {
doAddWithExistingValue(keySetView);
}
@Benchmark
public void _6_copyOnWriteArraySet_add_with_existing_value() {
doAddWithExistingValue(copyOnWriteArraySet);
}
// Reading
@Benchmark
public void _7_keySetView_iterate() {
String res = doIterate(keySetView);
}
@Benchmark
public void _8_copyOnWriteArraySet_iterate() {
String res = doIterate(copyOnWriteArraySet);
}
@Benchmark
public void _9_keySetView_contains() {
boolean res = doContains(keySetView);
}
@Benchmark
public void _010_copyOnWriteArraySet_contains() {
boolean res = doContains(copyOnWriteArraySet);
}
// Writing
private void doRemove(Set<String> set) {
set.remove(getRandomString());
}
private void doAddWithNewValue(Set<String> set) {
set.add(getRandomString() + set.size());
}
private void doAddWithExistingValue(Set<String> set) {
set.add(getRandomString());
}
// Reading
private String doIterate(Set<String> set) {
String result = "";
for (String string : set) {
result += string;
}
return result;
}
private boolean doContains(Set<String> set) {
return set.contains(getRandomString());
}
// Random value with seed
private String getRandomString() {
return String.valueOf(random.nextInt(100));
}
}
结果出来了(分数越低越好)。
写入操作:
Benchmark Mode Cnt Score Error Units SetBenchmark._1_keySetView_remove avgt 61,659 ns/op SetBenchmark._2_copyOnWriteArraySet_remove avgt 249,976 ns/op SetBenchmark._3_keySetView_add_with_new_value avgt 240,589 ns/op SetBenchmark._4_copyOnWriteArraySet_add_with_new_value avgt 30691,318 ns/op SetBenchmark._5_keySetView_add_with_existing_value avgt 84,472 ns/op SetBenchmark._6_copyOnWriteArraySet_add_with_existing_value avgt 473,592 ns/op
读取操作:
Benchmark Mode Cnt Score Error Units SetBenchmark._7_keySetView_iterate avgt 13603,012 ns/op SetBenchmark._8_copyOnWriteArraySet_iterate avgt 13626,146 ns/op SetBenchmark._9_keySetView_contains avgt 53,081 ns/op SetBenchmark._10_copyOnWriteArraySet_contains avgt 250,401 ns/op
只有迭代器的两个 Set
实现的读取速度一样快。
在任何情况下 CopyOnWriteArraySet
的写入都慢得多,但是当我们 add()
一个不存在的值时,情况更糟。