使用并发工具类库时的坑

2 篇文章 0 订阅

ThreadLocal

ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景。使用Springboot创建一个web项目,我们使用ThreadLocal来保存用户信息。
在这里插入图片描述

问题

按常理来说,第一次获取ThreadLocal的值时应该为null,但是在多线程的情况下可能会有值。原因是因为程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的,所以存在线程的服用,因此我们获取的ThreadLocal的值是上一次线程调用所存储的值,即上一个用户的信息。(并不能认为没有显式开启多线程就不会有线程安全问题。)

解决方案

在代码的 finally 代码块中,显式清除ThreadLocal 中的数据。这样一来,新的请求过来即使使用了旧线程也不会获取到错误的用户信息了。
在这里插入图片描述

ConcurrentHashMap

ConcurrentHashMap是一个高性能的线程安全的哈希表容器,但是它只能保证原子性读写操作是线程安全的。并不代表着在多线程下使用它就不会有线程安全的问题。

  • 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁
  • 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。
  • 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。

在使用一些类库提供的高级工具类时,开发人员可能还是按照旧的方式去使用这些新类,因为没有使用其特性,所以无法发挥其威力。
现在有一个场景:使用 Map 来统计 Key 出现次数。

老方法

	private Map<String, Long> normaluse() throws InterruptedException {
        ConcurrentHashMap<String, Long> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
        ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
        forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
                    String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);
                    //************************
                    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);
        return freqs;
    }

直接通过锁的方式锁住 Map,然后做判断、读取现在的累计值、加1、保存累加后值的逻辑。这段代码在功能上没有问题,但无法充分发挥ConcurrentHashMap的威力。

新方法

	private Map<String, Long> gooduse() throws InterruptedException {
        ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
        ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
        forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
                    String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);
                    //判断key所对应的value是否存在,若不存在则创建一个LongAdder对象返回作为它的value,最后自增
                    freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
                    //************************
                }
        ));
        forkJoinPool.shutdown();
        forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
        return freqs.entrySet().stream()
                .collect(Collectors.toMap(
                        e -> e.getKey(),
                        e -> e.getValue().longValue())
                );
    }

LongAdder,是一个线程安全的累加器,因此可以直接调用其 increment 方法进行累加。这样在确保线程安全的情况下达到极致性能,把之前 7 行代码替换为了 1 行,且性能更高。 新方法的关键点在于使用了computeIfAbsent,它是通过CAS算法保证写入数据的原子性,比直接加锁更高效。

总结

  1. 以为 ThreadLocal 在线程之间做了隔离不会有线程安全问题,没想到线程重用导致数据串了。
  2. 没有充分了解并发工具的特性,还是按照老方式使用新工具导致无法发挥其性能。
  3. 没有了解清楚工具的适用场景,在不合适的场景下使用了错误的工具导致性能更差。比如:没有理解 CopyOnWriteArrayList 的适用场景,把它用在了读写均衡或者大量写操作的场景下,导致性能问题。对于这种场景,可以考虑直接使用普通的 List。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值