我们一起复习Java并发(五):并发容器和同步器

本文深入探讨了Java并发编程中的并发容器,包括ConcurrentHashMap、ConcurrentSkipListMap的性能对比,以及ThreadLocalRandom的正确使用。通过示例展示了ConcurrentHashMap的高效并发操作,如computeIfAbsent(),并警告了其潜在的误用情况。此外,文章还测试了CopyOnWriteArrayList、CountDownLatch、Semaphore、CyclicBarrier和Phaser等同步器的性能和用法,阐述了它们在并发控制中的不同作用。最后,提醒开发者注意并发容器和同步器的正确使用,避免常见错误。
摘要由CSDN通过智能技术生成

本节我们先会来复习一下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();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值