详解常用集合和映射中的线程安全问题

news2025/3/9 16:50:21

1. 前言

  • 在 Java 中,集合和映射是常用的数据结构,它们分为线程安全和线程不安全两类。
  • 我们常用的集合包括:ArrayList、HashSet、CopyOnWriteArrayList、CopyOnWriteArraySet。常用的映射包括:HashMap、ConcurrentHashMap、Hashtable(Properties)。
  • 下文我们来详细分析其中的线程安全问题。

2. 线程不安全的集合与映射类

2.1 ArrayList
2.1.1 特点

ArrayList 是基于动态数组实现的,它允许存储 null 元素,并且可以根据需要动态调整数组大小。在单线程环境下,ArrayList 的性能表现良好,支持快速随机访问。
在这里插入图片描述

2.1.2 ArrayList 线程不安全的原因

ArrayList 的核心是一个动态数组,当元素数量超过数组容量时,会进行扩容操作。同时,它内部有一个 modCount 变量用于记录集合结构被修改的次数,以保证迭代器的一致性。
在这里插入图片描述
在多线程环境下,多个线程可以同时访问和修改 ArrayList 的内部状态,而 ArrayList 并没有对这些操作进行同步控制,这就可能导致数据不一致数组越界并发修改异常等问题。

2.1.2.1 多线程同时调用 add 方法可能出现的问题

1. 数据覆盖问题
ArrayListadd 方法的核心逻辑大致如下:
在这里插入图片描述

在多线程环境下,假设有两个线程 AB 同时调用 add 方法。当线程 A 和线程 B 同时执行到 ensureCapacityInternal(size + 1) 时,由于此时 size 的值相同,它们都会认为数组容量足够,不需要进行扩容。然后线程 A 先执行 elementData[size++] = e,将元素添加到数组末尾并将 size 加 1。同时线程 B 执行 elementData[size++] = e,线程 B 的元素将覆盖线程 A 添加的元素,导致数据丢失。
2. 数组越界问题
ArrayList 在进行扩容时,会创建一个新的数组,并将原数组中的元素复制到新数组中。如果多个线程同时触发扩容操作,可能会导致数组越界异常。
在这里插入图片描述
例如,当数组容量为 10,size 为 9 时,两个线程同时调用 add 方法,都发现需要扩容。其中一个线程先完成扩容操作,将数组容量扩大到 15。而另一个线程并不知道数组已经扩容,仍然按照原来的容量进行操作,当它尝试将元素添加到数组的第 10 个位置时,就会抛出 ArrayIndexOutOfBoundsException 异常。

3. ConcurrentModificationException 问题
ArrayList 的迭代器是快速失败(fail-fast)的,它会通过 modCount 变量来检测集合结构是否被修改。在多线程环境下,如果一个线程正在使用迭代器遍历 ArrayList,而另一个线程同时调用 add 方法修改了集合的结构,迭代器会检测到 modCount 的变化,从而抛出 ConcurrentModificationException 异常。
在这里插入图片描述

2.2 LinkedList
2.2.1 特点

LinkedList 是基于双向链表实现的,它实现了 ListDeque 接口,因此可以作为列表、栈或队列使用。它在插入和删除元素时性能较好,尤其是在列表的头部或尾部。
在这里插入图片描述

2.2.2 线程不安全的体现

在这里插入图片描述

2.3 HashMap
2.3.1 特点
  • HashMap 是基于哈希表实现的,它允许使用 null 作为键和值。它提供了快速的插入、查找和删除操作,平均时间复杂度为 O(1)。
    在这里插入图片描述
2.3.1.1 红黑树

红黑树的节点继承了LinkedHashMap.Entry<K,V>,继而继承了Node。在这里插入图片描述
在这里插入图片描述

2.3.2 HashMap线程不安全的原因

HashMap 线程不安全主要体现在多线程环境下可能出现数据丢失、死循环、数据不一致等问题,以下是具体原因分析:
1. 数据结构与操作特点
HashMap 是基于哈希表实现的,通过哈希函数计算键的哈希值来确定键值对在数组中的存储位置。在进行插入、删除和查找操作时,通常需要先计算哈希值,再根据哈希值确定在数组中的索引位置。

  • 哈希冲突处理:当不同的键计算出相同的哈希值时,就会发生哈希冲突。HashMap 采用链地址法来解决哈希冲突,即每个数组元素是一个链表的头节点,当发生哈希冲突时,新的键值对会以链表节点的形式插入到对应位置的链表中。
  • 操作的非原子性HashMap 的插入、删除等操作通常不是原子的,涉及多个步骤。例如,在插入一个键值对时,需要计算哈希值、确定索引位置、检查是否存在相同键、更新链表或数组等多个操作。在多线程环境下,如果多个线程同时进行插入操作,这些操作可能会相互干扰,导致数据不一致或丢失
    在这里插入图片描述
    2. 多线程并发操作问题
  • 扩容时的并发问题
    • 多线程同时扩容:在多线程环境下,可能会出现多个线程同时检测到 HashMap 需要扩容的情况。每个线程都可能会执行扩容操作各自创建新的更大容量的数组,并尝试将原数组中的键值对重新哈希到新数组中。这会导致键值对在多个线程之间被重复处理或丢失,最终 HashMap 中的数据可能会出现混乱或不完整。
      在这里插入图片描述
    • 数据迁移冲突:在扩容过程中,需要将原数组中的数据重新分配到新的数组中。如果两个线程同时进行数据迁移,可能**会导致链表结构被破坏。**例如,线程A正在处理某个链表节点,将其迁移到新数组的某个位置,而线程B同时也在处理同一个链表节点,可能会导致链表的指针指向错误,最终导致数据丢失或无法正确访问。
  • 插入操作时的并发问题
    • 哈希冲突导致数据覆盖:当多个线程同时向 HashMap 中插入键值对时,如果这些键值对发生哈希冲突,即它们的哈希值相同,可能会导致数据覆盖。例如,线程A和线程B同时插入两个不同的键值对,但它们的哈希值相同,都应该插入到同一个链表位置。由于线程执行的不确定性,可能会导致其中一个键值对覆盖另一个键值对,造成数据丢失。
    • 链表插入顺序混乱:在处理哈希冲突时,新的键值对需要插入到链表的头部或尾部。在多线程环境下,多个线程可能同时尝试向同一个链表插入节点,这可能会导致链表的插入顺序混乱,破坏链表的结构,进而影响后续的查找和遍历操作。
  • 迭代过程中的并发修改问题:当一个线程正在对 HashMap 进行迭代操作(如遍历 HashMap 中的所有键值对)时,另一个线程可能同时对 HashMap 进行了插入、删除或修改操作。这可能会导致迭代器出现异常行为,如抛出 ConcurrentModificationException 异常,或者迭代到不完整或不正确的数据。
    在这里插入图片描述
2.4 HashSet
2.4.1 特点

HashSet 是基于 HashMap 实现的,它不允许存储重复元素,并且不保证元素的顺序。
在这里插入图片描述
在这里插入图片描述

3. 线程安全的集合与映射类

3.1 Vector
  • 特点Vector 也是基于动态数组实现的,和 ArrayList 类似,但它是线程安全的。Vector 的方法大多使用 synchronized 关键字修饰,保证了在多线程环境下的操作是线程安全的。
    在这里插入图片描述
3.2 Hashtable
  • 特点Hashtable 是基于哈希表实现的,和 HashMap 类似,但它是线程安全的。Hashtable 不允许使用 null 作为键或值,并且它的方法也是同步的。
  • 性能问题:同样由于使用了同步机制,Hashtable 的性能不如 HashMap,尤其是在高并发场景下。
    在这里插入图片描述
这里补充说明一下为什么Properties要使用Hashtable而不是HashMap?

Properties 类使用 Hashtable 而不是 HashMap 主要有历史原因、线程安全以及对属性文件处理的适配性等方面的考虑,具体如下:

历史原因

  • Properties 类诞生于Java早期版本,在当时,Hashtable 是Java中提供的主要哈希表实现类。Properties 类被设计用于处理配置文件等属性信息,在其最初设计时选择了当时已有的 Hashtable 作为底层存储结构,这在一定程度上是为了利用 Hashtable 已有的功能和特性,并且与当时的Java生态和设计理念相契合。

线程安全

  • Hashtable 是线程安全的哈希表实现,它的方法大多是通过 synchronized 关键字来实现同步的,这意味着在多线程环境下,多个线程可以安全地访问和操作 Hashtable,而不会出现数据不一致或并发访问错误等问题。
  • Properties 通常会在多个线程可能同时访问的场景中使用,例如在读取和修改配置文件属性时,多个线程可能需要同时访问 Properties 对象。使用 Hashtable 作为底层结构可以确保在这些场景下的线程安全性,而不需要额外的同步措施。
  • 虽然 HashMap 也可以通过一些方式(如使用 Collections.synchronizedMap 方法)来实现线程安全,但这需要额外的代码和操作,相比之下,Hashtable 原生的线程安全特性更符合 Properties 对线程安全的需求。
3.3 ConcurrentHashMap
3.3.1 特点

ConcurrentHashMap 是线程安全的哈希表实现,它采用了 CAS(Compare-And-Swap)和 synchronized 关键字,在多线程环境下具有较高的并发性能。它允许使用 null 作为值,但不允许使用 null 作为键。
在这里插入图片描述

3.3.2 保障线程安全的措施

下面从初始化、插入、扩容和读取等方面详细解释其线程安全的实现机制。

3.3.2.1 初始化

ConcurrentHashMap初始化是懒加载的,在第一次插入元素时才会进行初始化操作。初始化操作使用 CAS 来保证只有一个线程可以成功初始化数组,避免多个线程同时初始化导致的问题。
在这里插入图片描述

在上述代码中,U.compareAndSwapInt 是一个 CAS 操作,通过比较 SIZECTL 的值是否等于 sc,如果相等则将其更新为 -1,表示当前线程正在进行初始化操作。

3.3.2.2 插入操作

插入元素时,ConcurrentHashMap 首先计算键的哈希值,然后找到对应的桶。对于不同的情况,采用不同的线程安全策略:

  • 桶为空:使用 CAS 操作将新节点插入到桶中,确保只有一个线程可以成功插入。
    在这里插入图片描述
  • 桶不为空且是链表结构:使用 synchronized 关键字对链表头节点加锁,然后遍历链表进行插入操作,保证同一时间只有一个线程可以修改该链表。
    在这里插入图片描述
  • 桶不为空且是红黑树结构:同样使用 synchronized 关键字对红黑树的根节点加锁,然后进行插入操作。

fh的含义

  • fh 代表当前桶的头节点的哈希值,代码里一般是通过 f.hash 获取的,这里的 f 就是当前桶的头节点。

  • 不同的哈希值有不同的含义:
    fh >= 0:表示当前桶存储的是链表结构。因为链表节点的哈希值通常是正常计算得到的,为非负数。
    fh == MOVED:MOVED 是一个常量(值为 -1),代表当前桶的头节点是 ForwardingNode,意味着该桶已经完成迁移,正在进行扩容操作。
    fh == TREEBIN:TREEBIN 是一个常量(值为 -2),表示当前桶存储的是红黑树结构。
    fh == RESERVED:RESERVED 是一个常量(值为 -3),表示当前桶正在进行计算操作。

3.3.2.3 扩容操作

当元素数量达到一定阈值时,ConcurrentHashMap 会进行扩容操作。扩容操作是多线程协作完成的,每个线程负责迁移一部分桶的数据。在迁移过程中,使用 ForwardingNode 标记已经迁移的桶其他线程在访问这些桶时会协助进行迁移。通过这种方式,避免了多个线程同时修改同一个桶的数据,保证了扩容操作的线程安全。
在这里插入图片描述

3.3.2.4 读取操作

读取操作是无锁的,因为 ConcurrentHashMap 的节点使用 volatile 关键字修饰,保证了节点的可见性。当一个线程修改了节点的值,其他线程可以立即看到最新的值。因此,读取操作不需要加锁,提高了并发性能。
在这里插入图片描述
在这里插入图片描述

3.4. CopyOnWriteArrayList
3.4.1 特点

CopyOnWriteArrayList 是线程安全的列表实现,它采用了写时复制的策略。当进行写操作(如添加、删除元素)时,会创建一个新的数组副本,在副本上进行修改,然后将原数组引用指向新的数组。

  • 适用场景:适用于读多写少的场景,因为写操作的开销较大。
    在这里插入图片描述
3.4.2 有Lock控制并发为什么还需要写时复制?

CopyOnWriteArrayList 采用 ReentrantLock 保证并发安全的同时使用写时复制(Copy-On-Write)策略,主要是为了在保证线程安全的基础上,尽可能地提高读操作的性能,同时减少锁的粒度和对读操作的影响,下面从几个方面详细阐述其原因。

1. 读写分离以提升读性能

  • 读操作无锁:写时复制策略允许在进行读操作时无需加锁。当多个线程进行读操作时,它们可以直接访问当前的数组,不会因为锁的竞争而阻塞。这是因为读操作读取的是原数组的内容,而写操作会在新的数组副本上进行修改,两者相互独立
  • 高并发读场景优势:在实际应用中,很多场景下读操作的频率远远高于写操作。

2. 保证数据一致性和线程安全

  • 写操作加锁:虽然写时复制策略本身不能完全保证线程安全,但 CopyOnWriteArrayList 在写操作时使用 ReentrantLock 进行加锁。当一个线程进行写操作(如 addremove 等)时,会先获取锁,确保同一时间只有一个线程可以进行写操作。
  • 复制与替换:获取锁后,写操作会创建一个原数组的副本,在副本上进行修改。修改完成后,再将原数组的引用指向新的数组副本。这样可以保证在写操作过程中,读操作仍然可以访问原数组,不会受到写操作的影响,从而保证了数据的一致性和线程安全。

3. 减少锁的粒度和影响范围

  • 锁仅用于写操作CopyOnWriteArrayList 只在写操作时加锁,锁的粒度相对较小。相比一些传统的线程安全列表(如 Vector),Vector 的所有操作(包括读操作和写操作)都使用 synchronized 关键字进行同步,这会导致在高并发读场景下,大量线程会因为锁的竞争而阻塞,性能受到严重影响。
  • 降低锁的持有时间:写时复制策略使得写操作在副本上进行,减少了锁的持有时间。因为写操作只需要在创建副本和替换原数组引用时持有锁,而在实际的修改操作过程中不需要持有锁,从而降低了锁的竞争程度,提高了并发性能。

4. 简化并发控制逻辑

  • 避免复杂的读写锁机制:写时复制策略避免了使用复杂的读写锁机制。读写锁虽然可以提高并发性能,但需要更复杂的实现和管理,容易出现死锁等问题。而 CopyOnWriteArrayList 的写时复制策略通过简单的复制和替换操作,结合 ReentrantLock 进行写操作的同步,简化了并发控制逻辑,降低了开发和维护的难度。
3.5 CopyOnWriteArraySet
  • 特点CopyOnWriteArraySet 是线程安全的集合实现,它基于 CopyOnWriteArrayList 实现,不允许存储重复元素。同样采用写时复制策略。
  • 适用场景:适用于读多写少的场景。
    在这里插入图片描述
    在这里插入图片描述

这里注意一下CopyOnWriteArraySet虽然是HashSet的并发替代。然而两者底层实现毫无关联。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2312231.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机毕业设计SpringBoot+Vue.js车辆管理系统(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

【js逆向】iwencai国内某金融网站实战

地址&#xff1a;aHR0cHM6Ly93d3cuaXdlbmNhaS5jb20vdW5pZmllZHdhcC9ob21lL2luZGV4 在搜索框中随便输入关键词 查看请求标头&#xff0c;请求头中有一个特殊的 Hexin-V,它是加密过的&#xff1b;响应数据包中全是明文。搞清楚Hexin-V的值是怎么生成的&#xff0c;这个值和cooki…

【音视频 | AAC】AAC编码库faac介绍、使用步骤、例子代码

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

Unity摄像机跟随物体

功能描述 实现摄像机跟随物体&#xff0c;并使物体始终保持在画面中心位置。 实现步骤 创建脚本&#xff1a;在Unity中创建一个新的C#脚本&#xff0c;命名为CameraFollow。 代码如下&#xff1a; using UnityEngine;public class CameraFollow : MonoBehaviour {public Tran…

dp_走方格(包含dfs分析,记忆化搜索)

类似题目解析&#xff1a;dp_最长上升子序列&#xff08;包含dfs分析&#xff0c;记忆化搜索&#xff09;-CSDN博客 题目链接&#xff1a;2067. 走方格 - AcWing题库 题目图片&#xff1a; 分析题目&#xff08;dfs&#xff09; 这个题目说有一个行为n行&#xff0c;列为m列…

如何用Kimi生成PPT?秒出PPT更高效!

做PPT是不是总是让你头疼&#xff1f;&#x1f629; 快速制作出专业的PPT&#xff0c;今天我们要推荐两款超级好用的AI工具——Kimi 和 秒出PPT&#xff01;我们来看看哪一款更适合你吧&#xff01;&#x1f680; &#x1f947; Kimi&#xff1a;让PPT制作更轻松 Kimi的生成效…

K8S学习之基础十八:k8s的灰度发布和金丝雀部署

灰度发布 逐步扩大新版本的发布范围&#xff0c;从少量用户逐步扩展到全体用户。 特点是分阶段发布、持续监控、逐步扩展 适合需要逐步验证和降低风险的更新 金丝雀部署 将新版本先部署到一小部分用户或服务器&#xff0c;观察其表现&#xff0c;再决定是否全面推广。 特点&…

Java 深度复制对象:从基础到实战

目录 一、深度复制的概念二、实现深度复制的方法1. 使用序列化2. 手动实现深度复制 三、总结 在 Java 编程中&#xff0c;对象的复制是一个常见的需求。然而&#xff0c;简单的复制操作&#xff08;如直接赋值&#xff09;只会复制对象的引用&#xff0c;而不是创建一个新的对象…

【前端】webstorm创建一个导航页面:HTML、CSS 和 JavaScript 的结合

文章目录 前言一、项目结构二、HTML 结构三、CSS 样式四、JavaScript 功能五、现代化风格优化htmlcssjavascript运行效果 总结 前言 在现代网页开发中&#xff0c;一个良好的导航栏是提升用户体验的重要组成部分。在这篇文章中&#xff0c;我将向您展示如何创建一个简单而完整…

AI编程: 一个案例对比CPU和GPU在深度学习方面的性能差异

背景 字节跳动正式发布中国首个AI原生集成开发环境工具&#xff08;AI IDE&#xff09;——AI编程工具Trae国内版。 该工具模型搭载doubao-1.5-pro&#xff0c;支持切换满血版DeepSeek R1&V3&#xff0c; 可以帮助各阶段开发者与AI流畅协作&#xff0c;更快、更高质量地完…

第11章 web应用程序安全(网络安全防御实战--蓝军武器库)

网络安全防御实战--蓝军武器库是2020年出版的&#xff0c;已经过去3年时间了&#xff0c;最近利用闲暇时间&#xff0c;抓紧吸收&#xff0c;总的来说&#xff0c;第11章开始学习利用web应用程序安全&#xff0c;主要讲信息收集、dns以及burpsuite&#xff0c;现在的资产测绘也…

GitHub上传项目

总结&#xff08;有基础的话直接执行这几步&#xff0c;就不需要再往下看了&#xff09;&#xff1a; git init 修改git的config文件&#xff1a;添加:[user]:name你的github用户名 email你注册github的用户名 git branch -m master main git remote add origin 你的URL gi…

DIY Tomcat:手写一个简易Servlet容器

在Java Web开发领域&#xff0c;Tomcat堪称经典&#xff0c;它作为Servlet容器&#xff0c;承载着无数Web应用的运行。今天&#xff0c;我将带大家一同探索如何手写一个简易的Tomcat&#xff0c;深入理解其底层原理。 一、背景知识 在开始之前&#xff0c;我们需要对几个关键…

基于ssm的物资进销存(全套)

现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本货物进销管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&#…

【CVPR2025】 EVSSM:用状态空间模型高效去模糊

Efficient Visual State Space Model for Image Deblurring 论文信息 题目&#xff1a; Efficient Visual State Space Model for Image Deblurring 用于图像去模糊的高效视觉状态空间模型 源码&#xff1a;https://github.com/kkkls/EVSSM 创新点 提出了高效视觉状态空间模型…

动态规划--斐波那契类型

目录 前言 1 第N个斐波那契数 2 爬楼梯 3 三步问题 4 使用最小花费爬楼梯 5 解码方法 总结 前言 本篇所讲的几个题目都是与斐波那契数的做法与思路类似的题目&#xff0c;所以直接放在一块解决了。 同时&#xff0c;由于第一次接触动态规划&#xff0c;我们也会讲解一…

《Python实战进阶》No16: Plotly 交互式图表制作指南

No16: Plotly 交互式图表制作指南 Plotly是一款用来做数据分析和可视化的在线平台&#xff0c;功能真的是非常强大&#xff0c;它主要有以下特点&#xff1a; 图形多样化&#xff1a;在线绘制多种图形&#xff0c;比如柱状图、饼图、直方图、饼图、气泡图、桑基图、股票图、旭…

代码随想录算法训练营第22天 | 组合总和 分割回文串

39. 组合总和 39. 组合总和 - 力扣&#xff08;LeetCode&#xff09; 题目链接/文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;带你学透回溯算法-组合总和&#xff08;对应「leetcode」力扣题目&#xff1a;39.组合总和&#xff09;| 回溯法精讲&#xff01;_哔哩哔哩_…

DeepSeek 医疗大模型微调实战讨论版(第一部分)

DeepSeek医疗大模型微调实战指南第一部分 DeepSeek 作为一款具有独特优势的大模型,在医疗领域展现出了巨大的应用潜力。它采用了先进的混合专家架构(MoE),能够根据输入数据的特性选择性激活部分专家,避免了不必要的计算,极大地提高了计算效率和模型精度 。这种架构使得 …

lvgl在ubuntu中模拟运行

文章目录 前言具体的步骤 前言 lvgl是一个图像UI的开源框架&#xff0c;用于嵌入式的设备之中。 在学习lvgl时&#xff0c;我们最好是现在PC上模拟运行&#xff0c;所以我们学习lvgl的第一步可以说是在我们的电脑上搭建模拟的运行环境。 参考官方的操作 lvgl在ubuntu上模拟运…