本帖最后由 2000000 于 2023-3-18 16:29 编辑
Java 容器的线程安全性杂谈
为什么写本文?
容器在开发中使用广泛,然而在多线程环境下使用非线程安全的容器可能会引起一系列的线程安全问题,比如竞态条件、死锁、数据不一致等。
这些问题可能会导致性能下降或者数据错误,严重情况下可能会对系统的安全性产生影响。
注重容器的线程安全性是非常必要的。使用线程安全的容器可以保证多线程环境下容器的正确使用,提高应用程序的健壮性可靠性。
同时,线程安全容器的使用也可以提高应用程序的性能,减少线程竞争和锁冲突等问题。
在一些小型或个人项目中,可能并不会过多考虑线程安全的问题,而是更注重代码的简洁和易读性,忽略线程安全性问题,不出现约等于没有。
本文旨在介绍一些 Java 容器在多线程环境下的线程安全性,并比较不同容器的线程安全性和性能表现,简单分析实现原理。
笔者的话:
我水平有限,会尽量以较为简单的语言来进行说明。本文表述较为简单,有些地方可以更深入地探讨和讲解,读者可自行研究。
本文适合初学者进行阅读,对初学者来说可能会有一些帮助。
读者在阅读前应该对 并发编程有一些了解,起码需要有开发经验。
如果您在阅读中发现了任何错误欢迎指正! 大佬轻点喷。
如果本文对您有帮助,请让我知道,我很高兴听到这些信息!
关于 ArrayList
关于 HashMap
结语:
wow,很感谢您看到了这里,这只是一个杂谈,我很高兴如果这篇杂谈对您有帮助!:)关于其余容器可能会在我有空的时候随便写写,也许? 我很建议您阅读下面的 参考文献!
参考文献:
Java 容器的线程安全性杂谈
为什么写本文?
容器在开发中使用广泛,然而在多线程环境下使用非线程安全的容器可能会引起一系列的线程安全问题,比如竞态条件、死锁、数据不一致等。
这些问题可能会导致性能下降或者数据错误,严重情况下可能会对系统的安全性产生影响。
注重容器的线程安全性是非常必要的。使用线程安全的容器可以保证多线程环境下容器的正确使用,提高应用程序的健壮性可靠性。
同时,线程安全容器的使用也可以提高应用程序的性能,减少线程竞争和锁冲突等问题。
在一些小型或个人项目中,可能并不会过多考虑线程安全的问题,而是更注重代码的简洁和易读性,忽略线程安全性问题,不出现约等于没有。
本文旨在介绍一些 Java 容器在多线程环境下的线程安全性,并比较不同容器的线程安全性和性能表现,简单分析实现原理。
笔者的话:
我水平有限,会尽量以较为简单的语言来进行说明。本文表述较为简单,有些地方可以更深入地探讨和讲解,读者可自行研究。
本文适合初学者进行阅读,对初学者来说可能会有一些帮助。
读者在阅读前应该对 并发编程有一些了解,起码需要有开发经验。
如果您在阅读中发现了任何错误欢迎指正! 大佬轻点喷。
如果本文对您有帮助,请让我知道,我很高兴听到这些信息!
关于 ArrayList
为什么 ArrayList 线程不安全? 如果多个线程同时读取、写入同一个 ArrayList,就可能会发生 竞态条件 (Race Condition)。 例如,如果一个线程正在读取 ArrayList 中的某个元素,而另一个线程正在删除这个元素,那么读操作就可能读取到一个被删除的元素,导致数据不一致或抛出异常。 什么是 "竞态条件 (Race Condition)" ? 当多个线程访问同一个资源时,由于线程执行顺序不确定,可能导致数据出现不确定性。在这种情况下,一般就说发生了 "竞态条件"。 这一段代码线程安全吗?
它不是线程安全的! 这段代码会出现竞态条件,即使两个线程操作不同的部分,也会出现问题!在这个例子中,线程A 向列表中添加元素,线程B 从列表中删除元素,因此线程A和线程B都会修改列表的内容。 当线程A和线程B同时访问列表时,可能会发生以下情况:
运行十五次,最终结果如下:
如果代码是线程安全的,那么最终输出的 list.size 应为 0。 因为线程B 不停地从列表中删除元素,当线程A 将所有元素添加到列表中后,线程B 会删除所有的元素,使得列表变为空。 在运行第 1、3、5、13 次很明显出现了数据不一致的问题。 使用 Vector? 使用 Vector 是很常见的解决方法之一,但是这真的推荐吗...? Vector 是线程安全的,这没错,它的方法都使用了 synchronized 关键字来保证同步。因此多个线程可以同时访问 Vector,而不需要担心线程安全问题。 但是,在高并发的情况下,这种同步机制会带来较大的性能开销,导致 Vector 的性能比较差。这真的不推荐。让我们来看看 add、remove、get 方法源码。
我们可以看到 Vector 使用 synchronized 关键字来保证线程安全,这是一种独占锁的机制,即在同一时间只能有一个线程获得该锁,其他线程需要等待。 在高并发的情况下,由于多个线程需要对 Vector 进行操作,这会导致大量的线程阻塞等待,从而影响程序的性能。 另外,Vector 内部是通过数组来存储元素的,当数组已经满了之后,需要对数组进行扩容。 在扩容时,需要新建一个数组,并将原数组中的元素复制到新数组中。这个操作非常消耗性能,特别是在数据量比较大的情况下,会导致严重的性能问题。 而且,由于 Vector 进行同步处理,这会导致多个线程需要等待其他线程完成扩容操作才能进行后续的操作。 使用 CopyOnWriteArrayList? 这也是常见的解决方法之一,该类是线程安全的,支持高并发的读写操作。 但是,CopyOnWriteArrayList 的性能开销可能会比较高,因为每次写操作都会创建一个新的数组来保存数据,这可能会对内存使用产生一定的影响。 而且,由于写操作会进行复制,所以写操作的速度也会比较慢。接下来让我们分析一下 CopyOnWriteArrayList set 方法源码。
可以看到,每个写操作都需要获取一个独占锁,然后将当前数组复制一份进行修改,最后将原数组替换为复制后的数组。 由于每次写操作都会复制一份数组,所以在写操作较频繁的情况下,会带来一定的性能开销和内存消耗。 因此,在 读多写少 的情况下,CopyOnWriteArrayList 的性能表现会比较好,但在读多写多的情况下,绝对不是最佳选择。 在线程安全的情况下获得更好的性能? 解决方法? 接下来让我们分别分析 ConcurrentLinkedQueue 与 ConcurrentLinkedDeque。 ConcurrentLinkedQueue 是高效的、线程安全、无|界、非阻塞队列。它的高效主要体现在以下几个方面:
下面是简要的 ConcurrentLinkedQueue 部分源码分析:
我们逐个分析方法:
相比之下,CopyOnWriteArrayList 为了支持元素快速查找和删除,使用了可重入锁 (ReentrantLock),保证修改操作互斥,从而保证了线程安全性。频繁的加锁和解锁会降低性能,导致低效。 ConcurrentLinkedQueue 中的插入和删除操作都不需要锁,所以能够快速处理高并发 Qin (神奇的屏蔽 雾) 况,使用单向链表实现,减少对共享数据的访问,提高性能。 与 ConcurrentLinkedQueue 类不同,ConcurrentLinkedDeque 类实现双向队列,因此它支持在队列头部和尾部插入和删除元素。 ConcurrentLinkedDeque 的高效体现在每个节点可以同时保存它的前驱节点和后继节点。 ConcurrentLinkedDeque 节点都保存了前驱节点和后继节点的引用,在执行插入和删除操作时,更新前驱节点和后继节点的引用即可。 而 ConcurrentLinkedQueue 节点只保存了后继节点的引用,在执行插入和删除操作时,需更新队头和队尾节点的引用,需使用 CAS 操作,相对更加耗时。 ConcurrentLinkedDeque 比 ConcurrentLinkedQueue在一些场景下更加高效。在此便不看源码了,如有有兴趣的开发者可以自行了解。 总而言之: 如果您需要双向队列来支持在头部和尾部插入和删除元素,可以选择使用 ConcurrentLinkedDeque。如果只需要在队列尾部插入和删除元素,可以选择使用 ConcurrentLinkedQueue。 |
关于 HashMap
为什么 HashMap 线程不安全? 因为它的操作不是原子性的,多线程同时进行 put 或 remove 操作可能会导致数据的不一致性或死锁。 这一段代码线程安全吗?
这个例子创建了两个线程,每个线程都往 HashMap 中插入了 10000 个键值对。 在这个例子中,若线程安全,则最终 HashMap 的大小为 10000。让我们运行它,最终结果如下:
稳定发挥,这是由于两个线程可能会同时插入具有相同的键的值,这会导致其中一个线程的插入操作被覆盖。 ConcurrentHashMap! 常见的解决方法之一 ConcurrentHashMap,它是线程安全的。 Segment? 不,它只是早期的实现方式,现在的实现方式已经不再使用 Segment 了。 当前实现使用了一种基于分段锁的技术,将哈希表分为多个桶,并在每个桶上使用独立的锁来实现线程安全性。 这种分段锁的设计可以保证在并发环境下,每个线程只需要获取相应的桶的锁,而不是对整个哈希表进行加锁,提高了并发性能 以下是 ConcurrentHashMap putVal 方法的源代码:
可以看出,ConcurrentHashMap 的插入操作使用了非常复杂的逻辑,并且涉及到了多线程之间的协作,非常复杂,在此便不分析了。 通过合理地利用哈希算法和并发控制机制,ConcurrentHashMap 可以高效的支持并发访问。因此,ConcurrentHashMap 是一种非常高效的并发哈希表实现。 ConcurrentHashMap 的轻量级实现 - ConcurrentSkipListMap? ConcurrentSkipListMap 是一个基于跳表实现的线程安全的Map,比 ConcurrentHashMap 更适合于需要高效有序访问的并发场景,例如并发的数据范围查询。 如果您的业|务需要高效的有序访问并且不在乎内存消耗,那么 ConcurrentSkipListMap 可能更适合您。而如果您的业|务对内存消耗比较敏感,同时对有序性要求不是特别高,那么 ConcurrentHashMap 可能更合适。 Caffeine! Caffeine 是一个基于 Java8+ 的高性能本地缓存库,其支持异步加载、过期自动回收等高级功能。 且提供了更多的缓存策略和配置选项,Caffeine 采用了一些新的技术,如缓存统计和突发模式等,以实现更好的性能和稳定性。 在高并发、大规模数据场景下,其性能表现更加优异。Caffeine 相比于 ConcurrentHashMap 具有更多的功能和优势。 针对 Caffeine 的使用可参考我写的简易工具类,此类将构建一个最简单的 Caffeine 缓存对象
在使用此方法时,需要使用泛型类型指定缓存键和值的类型。例如 Cache<String, Integer> cache = buildCaffeineCache(); 将构建一个缓存,其键和值的类型分别为 String 和 Integer。 您还可以通过 Caffeine 构建器的不同方法来配置缓存的行为。以下是 Caffeine 构建器常用方法及其含义:
这些方法中的大多数都接受一个 Duration 参数,该参数指定了时间段。例如,Duration.ofMinutes(10) 指定了 10 分钟的时间段。 此外,还有一些其他的方法用于更细粒度的配置,如 maximumWeight(long maximumWeight) 用于设置缓存的最大权重,weigher(Weigher<? super K, ? super V> weigher) 用于指定计算缓存项权重的方法。 在构建完缓存对象后,可以使用 Cache 接口提供的方法来操作缓存。例如,get(K key) 方法用于获取缓存中给定键的值,put(K key, V value) 方法用于将给定键值对添加到缓存中。 Caffeine: https://github.com/ben-manes/caffeine 温馨提示: 对于 Java 11 或更高版本,使用 3.x 否则使用 2.x. |
结语:
wow,很感谢您看到了这里,这只是一个杂谈,我很高兴如果这篇杂谈对您有帮助!:)关于其余容器可能会在我有空的时候随便写写,也许? 我很建议您阅读下面的 参考文献!
参考文献:
- ConcurrentHashMap源码分析
- ConcurrentHashMap底层分析
- ConcurrentHashMap的原理分析
- java多线程 ConcurrentLinkedQueue源码分析
- 秒懂:JCTool 的 Mpsc 高性能无锁队列 (史上最全+10W字长文)
本帖最后由 结冰的离季 于 2023-2-22 21:50 编辑
新手: 能跑就行
老手: 能跑就行
新手: 能跑就行
老手: 能跑就行

如果你想普及线程安全问题我建议你先解释下线程,最好是带上mc插件、mod的应用或例子。 我知道的一个例子是之前一个口令红包插件,在多人同时高频领取下可以超额领取。 比如插件可以普及下哪些情况是异步调用,在这种情况需要注意什么, 还是以上面的红包为例,监听 AsyncPlayerChatEvent 是异步调用的,但是插件作者使用的容器是线程不安全的、或者说对某一个成员变量不是volatile的 等等,最终导致了这种情况,然后应该怎么解决 就目前这些纯java知识来说在论坛普及不太开, 如果真想学不如去网上找个juc教程来的亲切易懂 |
本帖最后由 2000000 于 2023-2-22 22:10 编辑
感谢建议!
笔者的话 中有写到 "读者在阅读前应该对 并发编程有一些了解,起码需要有开发经验。"
我并没有想把这个帖子写成类似于教程,更多的是分析 (也许),并在分析过后给予一些建议。
这篇杂谈的表述已经尽可能较为简单一些,很多地方没有进行深入探讨
我应该会过段时间加入更多的实例问题分析,感谢建议~
结冰的离季 发表于 2023-2-22 21:42
新手: 能跑就行
老手: 能跑就行
感谢建议!
笔者的话 中有写到 "读者在阅读前应该对 并发编程有一些了解,起码需要有开发经验。"
我并没有想把这个帖子写成类似于教程,更多的是分析 (也许),并在分析过后给予一些建议。
这篇杂谈的表述已经尽可能较为简单一些,很多地方没有进行深入探讨
我应该会过段时间加入更多的实例问题分析,感谢建议~