在多线程编程的广袤天地里,并发工具类犹如一把把锋利的双刃剑,运用得当,可大幅提升程序的性能与效率,助力我们在复杂的业务场景中披荆斩棘;但倘若使用不当,不仅无法发挥其优势,反而会埋下诸多隐患,导致程序出现难以排查的错误和性能瓶颈。从线程安全工具类的微妙陷阱,到并发工具类特性的深度挖掘,再到使用场景的精准匹配,每一个环节都至关重要。接下来,让我们一同深入探讨并发工具类在使用过程中的要点与注意事项,为构建稳健、高效的多线程应用筑牢根基 。
使用线程安全的工具类并不等同于解决了所有线程安全问题。以ConcurrentHashMap
为例,虽然它能够保证原子性的读写操作是线程安全的,但在某些场景下,仍然可能出现线程安全问题。
场景示例:
假设有一个包含800个元素的Map
,现在需要再添加200个元素,这个添加操作由10个线程并发执行,每次线程的逻辑如下:
使用了ConcurrenntHashMap,先通过size方法获取到当前元素数量,计算出还需要添加多少个元素,然后通过putAll方法把缺少的元素添加进去。
在这种情况下,最终添加的元素数量可能会远大于200个。这是因为多个线程在读取map.size()
时可能会同时得到相同的值,从而导致多个线程都认为还有空间添加元素。
因此
还是拿ConcurrenntHashMap举例,假如现有如下逻辑:
并有如下代码:
public class Main {
private static int LOOP_COUNT = 1000000;
private static int THREAD_COUNT = 10;
private static int ITEN_COUNT = 10;
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap map = new ConcurrentHashMap<>(ITEN_COUNT);
ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
String key = "item" + ThreadLocalRandom.current().nextInt(ITEN_COUNT);
synchronized (map) {
if (map.contains(key)) {
map.put(key, map.get(key) + 1);
} else {
map.put(key, 1L);
}
}
}));
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
}
}
这样的作法实现的效果显然是没有问题,但是这并没有完全发挥ConcurrenntHashMap的全部威力,性能不佳。
为此我们可以做出如下优化:
public class Main {
private static int LOOP_COUNT = 1000000;
private static int THREAD_COUNT = 10;
private static int ITEN_COUNT = 10;
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap map = new ConcurrentHashMap<>(ITEN_COUNT);
ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
String key = "item" + ThreadLocalRandom.current().nextInt(ITEN_COUNT);
map.computeIfAbsent(key, k -> new LongAdder()).increment();
}));
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
}
}
在ConcurrenntHashMap中,使用computelfAbsent来做复合逻辑操作,判断key是否存在value,如果不存在则把Lambda表达式运行的结果放入map作为value,也就是创建了一个LongAdder对象,最后返回Value。
computelfAbsent方法返回的value是LongAdder,是一个线程安全的累加器,因此可以直接调用increment方法进行累加。
这样在确保线程安全的情况下,把性能也发挥到了极致,还减少了代码行数,唯一的额外操作就是还需要把LongAdder转化为Long类型。
例如现在需要来缓存大量的数据,并且数据变化比较频繁。
而你使用了CopyOnWriteArrayList来实现这个操作,这样就会造成严重的性能浪费问题。
CopyOnWriteArrayList
的核心实现原理是写时复制(Copy-On-Write) 。当执行添加、删除、修改等写操作时,它并不会直接在原数组上进行修改,而是先复制出一个新的数组,然后在新数组上完成相应的操作,最后将原数组的引用指向新数组。这种机制在一定程度上保证了读操作的高效性和线程安全性,因为读操作始终是在原数组上进行,无需加锁,避免了锁竞争带来的性能开销。
然而,在数据频繁变化的场景下,这种实现方式的弊端就会暴露无遗。每次写操作都要复制整个数组,这意味着大量的内存资源会被消耗在数组的复制上。随着写操作次数的增加,内存的占用会急剧上升,甚至可能导致内存溢出错误。
此外,频繁的数组复制操作还会占用大量的 CPU 时间,使得系统的整体性能大幅下降。比如,在一个实时数据处理系统中,需要不断地更新缓存中的数据以反映最新的业务状态。如果使用CopyOnWriteArrayList
作为缓存容器,随着数据更新频率的增加,系统的响应速度会越来越慢,无法满足实时性的要求。
因此,CopyOnWriteArrayList
显然并不适合数据频繁变动的场景。相反,它更适合应用于读操作远远多于写操作,或者期望在无锁状态下进行高效读操作的场景中。例如,在一些日志记录系统中,日志数据通常是被频繁读取用于分析和查询,而写入操作相对较少,此时使用CopyOnWriteArrayList
就能够充分发挥其读操作高效的优势。又比如,在一些配置信息的缓存场景中,配置信息在系统运行过程中很少发生变化,但会被多个线程频繁读取,CopyOnWriteArrayList
也能很好地满足这种需求。
综上所述,在使用并发工具类时,我们必须深入了解其底层的实现原理,只有这样,才能在众多的并发工具中做出正确的选择,将其应用于最适合的业务场景,从而实现高效、稳定的多线程编程。