本节我们先会来复习一下java.util.concurrent下面的一些并发容器,然后再会来简单看一下各种同步器。
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)
ConcurrentHashMap和ConcurrentSkipListMap的性能
首先,我们来测试一下ConcurrentHashMap和ConcurrentSkipListMap的性能。
前者对应的非并发版本是HashMap,后者是跳表实现,Map按照Key顺序排序(当然也可以提供一个Comparator进行排序)。
在这个例子里,我们不是简单的测试Map读写Key的性能,而是实现一个多线程环境下使用Map最最常见的场景:统计Key出现频次,我们的Key的范围是1万个,然后循环1亿次(也就是Value平均也在1万左右),10个并发来操作Map:
@Slf4j
public class ConcurrentMapTest {
int loopCount = 100000000;
int threadCount = 10;
int itemCount = 10000;
@Test
public void test() throws InterruptedException {
StopWatch stopWatch = new StopWatch();
stopWatch.start("hashmap");
normal();
stopWatch.stop();
stopWatch.start("concurrentHashMap");
concurrent();
stopWatch.stop();
stopWatch.start("concurrentSkipListMap");
concurrentSkipListMap();
stopWatch.stop();
log.info(stopWatch.prettyPrint());
}
private void normal() throws InterruptedException {
HashMap<String, Long> freqs = new HashMap<>();
ForkJoinPool forkJoinPool = new ForkJoinPool(threadCount);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, loopCount).parallel().forEach(i -> {
String key = "item" + ThreadLocalRandom.current().nextInt(itemCount);
synchronized (freqs) {
if (freqs.containsKey(key)) {
freqs.put(key, freqs.get(key) + 1);
} else {
freqs.put(key, 1L);
}
}
}
));
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
//log.debug("normal:{}", freqs);
}
private void concurrent() throws InterruptedException {
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>(itemCount);
ForkJoinPool forkJoinPool = new ForkJoinPool(threadCount);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, loopCount).parallel().forEach(i -> {
String key = "item" + ThreadLocalRandom.current().nextInt(itemCount);
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
}
));
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
//log.debug("concurrentHashMap:{}", freqs);
}
private void concurrentSkipListMap() throws InterruptedException {
ConcurrentSkipListMap<String, LongAdder> freqs = new ConcurrentSkipListMap<>();
ForkJoinPool forkJoinPool = new ForkJoinPool(threadCount);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, loopCount).parallel().forEach(i -> {
String key = "item" + ThreadLocalRandom.current().nextInt(itemCount);
freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
}
));
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
//log.debug("concurrentSkipListMap:{}", freqs);
}
}
这里可以看到,这里的三种实现:
- 对于normal的实现,我们全程锁住了HashMap然后进行读写
- 对于ConcurrentHashMap,我们巧妙利用了一个computeIfAbsent()方法,实现了判断Key是否存在,计算获取Value,put Key Value三步操作,得到一个Value是LongAdder(),然后因为LongAdder是线程安全的所以直接调用了increase()方法,一行代码实现了5行代码效果
- ConcurrentSkipListMap也是一样
运行结果如下:
可以看到我们利用ConcurrentHashMap巧妙实现的并发词频统计功能,其性能相比有锁的版本高了太多。
值得注意的是,ConcurrentSkipListMap的containsKey、get、put、remove等类似操作时间复杂度是log(n),加上其有序性,所以性能和ConcurrentHashMap有差距。
如果我们打印一下ConcurrentSkipListMap最后的结果,差不多是这样的:
可以看到Entry按照了Key进行排序。
ConcurrentHashMap的那些原子操作方法
这一节我们比较一下computeIfAbsent()和putIfAbsent()的区别,这2个方法很容易因为误用导致一些Bug。
- 第一个是性能上的区别,如果Key存在的话,computeIfAbsent因为传入的是一个函数,函数压根就不会执行,而putIfAbsent需要直接传值。所以如果要获得Value代价很大的话,computeIfAbsent性能会好
- 第二个是使用上的区别,computeIfAbsent返回是的是操作后的值,如果之前值不存在的话就返回计算后的值,如果本来就存在那么就返回本来存在的值,putIfAbsent返回的是之前的值,如果原来值不存在那么会得到null
写一个程序来验证一下:
@Slf4j
public class PutIfAbsentTest {
@Test
public void test() {
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
log.info("Start");
log.info("putIfAbsent:{}", concurrentHashMap.putIfAbsent("test1", getValue()));
log.info("computeIfAbsent:{}", concurrentHashMap.computeIfAbsent("test1", k -> getValue()));
log.info("putIfAbsent again:{}", concurrentHashMap.putIfAbsent("test2", getValue()));
log.info("computeIfAbsent again:{}", concurrentHashMap.computeIfAbsent("test2", k -> getValue()));
}
private String getValue() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return UUID.randomUUID().toString();
}
}