Java: 集合
Java 集合类有哪些Java中的集合类主要由Collection和Map这两个接口派生而出其中Collection接口又派生出三个子接口分别是Set、List、Queue。所有的Java集合类都是Set、List、Queue、Map这四个接口的实现类。Collection接口单列集合定义Collection是所有单列集合的根接口用于存储一系列不重复或重复的单个元素。分类List接口Δ特点元素有序取出数据按插入顺序元素可重复。Δ常见的实现类①ArrayList底层基于动态数组实现。特点是查询随机访问速度快因为可以通过索引直接定位但增删元素特别是中间位置效率相对较低因为可能会涉及到大量元素的移动。ArrayList类是非线程安全的。②LinkedList底层基于双向链表实现。特点是查询随机访问效率较低因为需要从头或尾遍历。但增删元素特别是首尾和中间位置效率高因为只需修改结点指针。LinkedList也是非线程安全的。③VectorVector类与ArrayList类似但它是线程安全的方法都加了synchronized关键字。但由于同步开销其性能通常低于ArrayList。Set接口Δ特点元素无序取出数据不保证插入顺序元素不可重复通过hashCode()和equals()方法判断。Δ常见的实现类①HashSet底层其实是HashMap。特点是增删改查效率高但元素无序。HashSet是非线程安全的。②LinkedHashSet继承自HashSet底层基于哈希表和链表实现。它在HashSet的基础上通过链表维护了元素的插入顺序有序。LinkedHashSet也是非线程安全的。③TreeSet底层基于红黑树实现。TreeSet类的特点是元素有序按自然排序或自定义比较器排序增删改查效率相对HashSet略低对数时间复杂度。TreeSet也是非线程安全的。Queue接口Δ特点遵循先进先出FIFO原则常用于模拟队列数据结构。Δ常见的实现类①PriorityQueue优先级队列队列元素根据其自然排序或自定义比较器进行排序每次取出的是优先级最高的元素。PriorityQueue是非线程安全的。注意 LinkedList 也是Queue的实现类提供offer()、poll()、peek()等队列操作方法Map接口双列集合特点双列集合用于存储键值对Key-Value Pair键Key是唯一的用于快速查找对应的值。值Value可以重复。一个键最多映射一个值。常见的实现类HashMap底层基于哈希表实现。它的特点是查询、增删效率高但元素键值对无序。HashMap是非线程安全的。LinkedHashMap继承自HashMap底层基于哈希表 和 双向链表实现。它在HashMap的基础上通过链表维护了键值对的插入顺序因此有序。LinkedHashMap是非线程安全的。TreeMap底层基于红黑树实现。它的特点是键值对有序按键的自然排序或自定义比较器排序增删改查效率相对HashMap略低。TreeMap是非线程安全的。Hashtable与HashMap类似但它是线程安全的方法都加了synchronized关键字性能较低是 Java 早期版本提供的集合类。Hashtable的键和值都不能为null。ConcurrentHashMap在JDK1.5之后提供的线程安全且高性能的双列集合实现。它通过分段锁JDK7或CAS synchronizedJDK8等机制提供了比Hashtable更好的并发性能。Array、ArrayList、LinkedList的区别是什么ArrayList vs Array长度ArrayList是动态数组的实现可以自动扩容。而Array是固定长度的数组。功能ArrayList提供了更多的功能比如自动扩容、增加和删除元素等Array只有length属性和下标访问无内置增删方法存储类型Array可以直接存储基本类型数据也可以存储对象。ArrayList中只能存储对象。对于基本类型数据需要使用其对应的包装类如Integer、Double等ArrayList vs LinkedList底层结构ArrayList基于动态数组LinkedList基于双向链表。使用特点ArrayList 特点是查询随机访问速度快因为可以通过索引直接定位但增删元素特别是中间位置效率相对较低因为可能会涉及到大量元素的移动。Linked 特点是查询随机访问效率较低因为需要从头或尾遍历。但增删元素特别是首尾和中间位置效率高因为只需修改结点指针。内存占用ArrayList 只需存储元素本身内存紧凑LinkedList 每个节点额外存储前后指针内存占用更大。扩容当容量不足以容纳更多元素时ArrayList会自动扩容这个过程涉及创建新数组和复制旧数组的内容有一定的开销。而LinkedList只需要创建一个新的节点对象把它和前后讲点关联起来就行。使用场景如果需要频繁随机访问优先使用ArrayList。如果主要是头部或中间插入删除且对访问性能要求不高可考虑LinkedList。实际开发中绝大多数场景下ArrayList是首选因为它综合性能好、内存更省除非有明确的链表操作需求。ArrayList 扩容机制ArrayList 的底层是一个Object[]数组。扩容的本质就是创建一个新的、容量更大的数组然后将原数组中的元素复制到新数组中最后让 ArrayList 内部的数组引用指向这个新数组。具体来说初始化容量如果使用无参构造器new ArrayList()创建一个ArrayList 对象在JDK 1.8及以后初始容量是0而不是10JDK 1.7。只有在第一次调用add()方法时会通过ArrayList中grow()方法将容量懒加载为默认的10。当然也可以使用带初始容量的构造器new ArrayList(int initialCapacity)来指定初始大小。扩容时机当调用add()方法添加元素且当前元素个数size 1 当前内部数组的长度时就会触发扩容。扩容计算ArrayList扩容的计算是在一个grow()方法里面先尝试将数组扩大为原数组的1.5倍。边界处理如果 1.5 倍后的容量仍然小于所需的最小容量例如addAll一次性加入大量元素那么新容量直接取所需的最小容量。如果新容量超过了定义的最大数组长度MAX_ARRAY_SIZE则会进行hugeCapacity()处理最终可能将容量设为Integer.MAX_VALUE或者 抛出OOM错误所需最小是int数值溢出的值复制若新的容量满足需求会调用一个Arrays.copyof方法, 将所有的元素从旧数组复制到新数组中最后让 ArrayList 内部的数组引用指向这个新数组总结由于扩容涉及数组复制时间复杂度O(n)频繁扩容会影响性能。所所以说建议使用ArrayList(int initialCapacity)构造器来指定初始容量避免多次扩容。为什么是1.5倍选择1.5 倍即右移一位oldCapacity 1是为了在时间与空间之间做平衡。如果增长倍数太小如 1.1 倍扩容次数频繁复制成本高。如果增长倍数太大如 2 倍可能会浪费较多内存空间。1.5 倍既能保证均摊下来add操作的时间复杂度依然是O(1)又能避免过度的内存浪费。HashMap底层实现在JDK 1.8之前HashMap由数组和链表组成当发生哈希冲突时多个元素会以链表的形式存储在同一个数组位置。JDK 1.8开始引入了红黑树当链表长度大于等于8时数组长度大于等于64时链表会转换成红黑树以提高搜索效率树节点大小小于等于6时就会变成链表因为红黑树在节点数量较大时优势才会明显体现出来节点数少的时候红黑树的维护会引来额外开销。为什么链表大小超过 8 会自动转为红黑树小于 6 时重新变成链表根据泊松分布 在负载因子默认为0.75的时候单个hash槽内元素个数为8的概率小于百万分 之一所以将7作为一个分水岭 等于 7 的时候不转换大于等于 8 的时候才转换成红黑树小于等于 6 的时候转化为链表。这样可以防止树和链表频繁互相转换为什么引入红黑树而不是其他树是因为红黑树具有以下几点性质不追求绝对的平衡插入或删除节点时允许有一定的局部不平衡相较于AVL树的绝对自平衡减少了很多性能开销红黑树是一种自平衡的二叉搜索树因此插入和删除操作的时间复杂度都是O(log n)HashMap读和写的时间复杂度是多少读在最佳情况下读操作的时间复杂度是 O(1)最坏情况下发生哈希冲突链表为O(n), 红黑树为O(log n)。写理想情况与读操作类似如果哈希函数分布均匀写操作的时间复杂度也是 O(1)。处理哈希冲突如果发生哈希冲突需要在链表尾部添加新元素或将链表转换为红黑树。在这种情况下写操作的时间复杂度可能达到 O(n)其中 n 是链表的长度。解决Hash冲突的方法有哪些 HashMap是如何解决 Hash 冲突的在哈希表散列表中解决冲突主要有四大类方法链地址法拉链法原理将哈希表的每个位置桶看作一个链表或红黑树。当多个元素映射到同一个桶时将它们放在该桶的链表/树中。优点处理冲突简单平均查找时间短删除节点容易。缺点需要额外的指针存储空间。开放地址法原理当冲突发生时通过探测序列去寻找下一个空闲的桶。常见探测方式线性探测依次往后找、二次探测12,−12,22...1^2, -1^2, 2^2...12,−12,22...、双重散列。优点数据都存储在数组内没有链表指针开销。缺点删除节点麻烦通常需标记为已删除且容易产生“聚集”现象性能退化为O(n)。再哈希法原理当发生冲突时不是简单地线性移动一个固定步长而是使用第二个哈希函数计算出一个新的步长再根据这个步长寻找下一个空闲桶。优点不易发生聚集。缺点计算时间开销较大。建立公共溢出区原理将哈希表分为基本表和溢出表冲突的元素都放入溢出表。HashMap 主要采用“链地址法”并引入了“红黑树”进行优化同时配合“高位扰动”和“扩容后重新计算节点哈希”来减少冲突。高位扰动将 hashCode 的高 16 位与低 16 位进行异或运算使哈希分布更均匀。扩容后重新计算节点哈希位置节点数量超过了装载因子与数组长度的乘积会进行扩容所有元素会重新计算新的桶位置HashMap 的 put 方法流程初始化检查首先检查数组table是否为null或长度为0。如果是则调用resize()进行初始化懒加载机制默认容量分配空间。计算索引并插入根据key键的hashcode值计算索引下标i。检查table 对应 的下标情况A无冲突如果table[i] null直接在该位置新建节点。情况B存在冲突如果table[i] ! null则先检查table[i]的首节点是否与当前key相同比较hashcode和equals。若相同则记录该节点准备覆盖。若不同则判断该节点是否为树节点TreeNode。如果是红黑树则执行红黑树的插入逻辑。如果是链表则遍历链表。若遍历过程中找到相同key则记录节点准备覆盖若未找到则在链表尾部插入新节点。链表树化在链表插入完成后判断当前链表的长度是否 8。如果 8调用treeifyBin方法。但该方法会进一步判断当前数组长度是否 64如果 64优先执行扩容resize。如果≥ 64则将链表转换为红黑树覆盖旧值如果在步骤2或遍历过程中找到了相同的key则用新值替换旧值并返回旧值方法结束。扩容判断如果是新增节点非覆盖则在插入成功后判断当前size是否大于扩容阈值threshold容量 * 负载因子。如果超过则进行resize扩容。HashMap 的扩容机制触发扩容的时机首次 put 时如果 table 为空或长度为 0会触发初始化扩容resize()分配默认容量或指定容量。指定容量的话会放大到2的幂的大小以便进行模计算的时候可以直接减一按位与来计算hashcode的模降低直接进行模计算的开销put 后 size threshold每次插入新键值对后检查当前元素个数是否大于阈值若大于则扩容。树化时当链表长度达到 8 且数组长度小于 64 时会先进行扩容而非直接树化JDK 1.8。扩容流程以 JDK 1.8 为例1. 计算新容量和新阈值新容量 旧容量 1翻倍新阈值 新容量 × loadFactor2. 创建新数组分配一个新的 数组长度为新容量3. 元素迁移重哈希遍历旧数组的每个桶链表或红黑树桶中只有一个元素时元素直接通过e.hash (newCap - 1)计算新索引放入新数组对应位置。e是正在遍历的元素e.hash 是Hashcode扰动后的值桶中是红黑树节点调用红黑树的拆解方法放入对应新数组位置桶中是多个链表节点遍历链表的多个元素并不是(e.hash (newCap - 1))由于新容量是旧容量的 2 倍元素的新索引要么是原索引要么是“原索引 旧容量”所以直接判断节点高一位 是1 或者 0 就可以判断该节点在新数组中的位置。而是利用hash oldCap来判断高位的情况将数组分为了低位链表和高位链表若结果为 0则元素在新数组中的位置不变仍在 旧索引的位置将其放入低位链表(尾插法不改变顺序)若结果不为 0则元素在新数组中的位置为 旧索引旧容量将其放入高位链表最后再分别插入到新的数组中JDK 1.7采用头插法并发可能形成环。迁移节点是直接遍历节点计算新下标插入到新数组开销更大。HashMap通过高16位与低16位进行异或运算来让高位参与散列提高散列效果HashMap控制数组的长度为2的整数次幂来简化取模运算提高性能对hashcode进行求余运算和让hashcode与数组长度-1进行位与运算是相同的效果HashMap通过控制初始化的数组长度为2的整数次幂、扩容为原来的2倍来控制数组长度一定为2的整数次幂。再优秀的hash算法永远无法避免出现hash冲突。hash冲突指的是两个不同的key经过hash计算之后得到的数组下标是相同的。解决hash冲突的方式很多如开放定址法、再哈希法、公共溢出表法、链地址法。HashMap采用的是链地址法jdk1.8之后还增加了红黑树的优化HashMap采用链地址法当发生冲突时会转化为链表当链表过长会转化为红黑树提高效率。HashMap对红黑树进行了限制让红黑树只有在极少数极端情况下进行抗压。装载因子决定了HashMap扩容的阈值需要权衡时间与空间一般情况下保持0.75不作改动HashMap扩容机制结合了数组长度为2的整数次幂的特点以一种更高的效率完成数据迁移同时避免头插法造成链表环。HashMap并不能保证线程安全在多线程并发访问下会出现意想不到的问题如数据丢失等HashMap1.8采用尾插法进行扩容防止出现链表环导致的死循环问题解决并发问题的的方案有Hashtable、Collections.synchronizeMap()、ConcurrentHashMap。其中最佳解决方案是ConcurrentHashMap上述解决方案并不能完全保证线程安全快速失败是HashMap迭代机制中的一种并发安全保证HashMap 是线程安全的吗多线程下会有什么问题如何实现线程安全HashMap 不是线程安全的。因为所有方法如put、get、resize等都没有使用synchronized或 加锁 进行保护。这些操作都不是原子性的在多线程环境下使用 HashMap 可能会出现以下问题死循环在 JDK 1.7 中HashMap 使用头插法插入元素当多个线程同时进行扩容操作时可能会导致环形链表的形成后续对同一哈希桶的get操作会陷入死循环。为了解决这个问题在 JDK 1.8 中采用了尾插法插入元素保持了链表元素的顺序避免了死循环的问题。数据覆盖当多个线程同时执行 put 操作时如果它们计算出的索引位置相同可能会造成前一个 key 被后一个 key 覆盖的情况从而导致元素的丢失。关于多线程安全的实现方案可以采取以下措施如果并发要求不高或对遗留代码兼容Hashtable是 JDK 1.0 的遗留类所有方法都用synchronized修饰属于全表锁(不允许null键值)性能较差新代码中不建议使用。Collections.synchronizedMap是 JDK 1.2 提供的包装类同样采用全表锁通过互斥对象(允许null键值)但并发能力依旧有限仅适合并发要求极低的场景。如果是高并发场景我通常会使用ConcurrentHashMap。它针对并发做了专门优化在 JDK 1.7 中采用分段锁Segment将数据分块减少锁粒度允许多个线程同时操作不同段。在 JDK 1.8 中优化为CAS synchronized保证线程安全性锁的粒度细化到数组的每个桶Node只有在操作同一个哈希桶时才会发生锁竞争并发性能是更优。总之在多线程环境下尽量不要直接使用 HashMap而是根据并发量的高低选择合适的封装类或ConcurrentHashMap。concurrentHashMap 如何保证线程安全concurrentHashMap相当于是HashMap的多线程版本它的功能本质上和HashMap没有什么区别。因为HashMap在并发操作时会出现死循环、数据覆盖等问题。 这些问题使用concurrentHashMap 就可以完美解决。JDK 1.7基本结构ConcurrentHashMap在JDK 1.7中使用的数组 加 链表的结构其中数组分为两类大树组Segment和 小数组HashEntry。大数组Segment包含了多个 小数组HashEntry每个HashEntry中有多条数据这些数据采用的是链表结构。![[Pasted image 20260326113850.png#pic_center|| 400]]分段锁多线程安全的实现是通过每个 Segment 独立继承自 ReentrantLock可重入锁锁的粒度是每个 Segment。 这保证同一时间只能有一个线程操作对应的节点因为ConcurrentHashMap的线程安全建立在Segment的加锁基础上所以被称为分段锁。JDK1.8因为大量数据的情况下使用链表结构需要遍历整个链表可能会造成数据访问效率降低。基本结构ConcurrentHashMap在JDK1.8中使用的是数组 加 链表 加 红黑树的方式实现与HashMap 在Jdk 1.8 一致当链表长度大于等于8数组长度大于等于64的时候会将链表转换为红黑树。通过CASsynchronized锁头节点实现并且缩小了锁的粒度到桶并发场景下操作性能也更高。具体CAS在插入元素、初始化数组等操作时使用无锁的 CAS 操作例如当数组桶为空时通过 CAS 将节点放入桶中避免加锁。synchronized当发生桶非空的写操作、树化的时、扩容节点迁移时时使用 synchronized 锁住当前桶的头节点可重入锁指的是同一个线程可以多次获取同一把锁而不会死锁内部通过计数器实现重复加锁与对应释放。CAS是Compare-And-Swap比较并交换的缩写在 Java 并发编程中是一种非常重要的无锁算法技术。它利用 CPU 的原子指令让多线程在更新共享变量时无需加锁就能保证线程安全。具体CAS 操作包含三个操作数内存位置 V要操作的变量在内存中的地址对应 Java 中的某个变量的引用。期望值 A线程认为该内存位置当前应该持有的值。新值 B线程希望将该内存位置更新为的值。执行逻辑当且仅当 V 的值等于 A 时CPU 才会原子地将 V 的值更新为 B否则不执行任何操作。无论是否更新成功都会返回 V 原来的值或返回布尔值表示是否成功。整个过程是一条 CPU 原子指令如 x86 架构下的 CMPXCHG中间不会被其他线程打断从而保证了操作的原子性。HashMap 和 HashSet 的区别HashMap和HashSet都是Java中的集合类但它们有以下区别存储内容不同HashSet实现了Set接口只存储对象HashTable实现了Map接口用于存储键值对底层实现HashSet的底层实际上就是HashMap。HashSet内部维护了一个HashMap的引用当我们向HashSet添加元素时实际上是把该元素作为HashMap的Key存入而 Value 则是一个统一的静态常量对象PRESENT空的new Object()永远不变。重复值HashSet不允许集合中有重复的值如果有重复的值会插入失败add()方法返回false。而HashMap的键不能重复但是值可以重复如果键重复会覆盖原来的值因为putVal()方法中onlyIfAbsent属性默认为false总的来说可以理解为HashSet 是‘阉割版’的 HashMap它只用了 HashMap 的 Key 部分Value 统一占位从而实现了元素的唯一性。// HashSet 的部分源码publicclassHashSetEextendsAbstractSetEimplementsSetE,Cloneable,java.io.Serializable{// 底层就是一个 HashMapprivatetransientHashMapE,Objectmap;// 所有元素共用的虚拟 valueprivatestaticfinalObjectPRESENTnewObject();// 空参构造直接 new 一个 HashMappublicHashSet(){mapnewHashMap();}// 带容量的构造publicHashSet(intinitialCapacity){mapnewHashMap(initialCapacity);}// 带容量负载因子publicHashSet(intinitialCapacity,floatloadFactor){mapnewHashMap(initialCapacity,loadFactor);}// add 核心逻辑put 判断返回值publicbooleanadd(Ee){// 这里的 map 就是上面 new 出来的 HashMap// 只有第一次存储该键值才会返回null// 重复存储会返回 旧Key的Value也就是PRESENT因为不为空所以false表示添加失败returnmap.put(e,PRESENT)null;}}// HashMappublicVput(Kkey,Vvalue){returnputVal(hash(key),key,value,false,true);}// HashMap 内部真正实现插入的方法finalVputVal(inthash,Kkey,Vvalue,booleanonlyIfAbsent,booleanevict){// ...// 关键逻辑在这里if(e!null){// 键已经存在VoldValuee.value;// onlyIfAbsent false → 直接覆盖// onlyIfAbsent true → 只有值为null才覆盖if(!onlyIfAbsent||oldValuenull)e.valuevalue;afterNodeAccess(e);returnoldValue;}// ...}HashMap 和 HashTable 的区别在 Java 中HashMap和HashTable都是对Map接口的实现用于存储键值对的集合。它们的区别在于线程安全Hashtable是同步的也就是线程安全的因为Hashtable使用了synchronized给整个方法加了锁。HashMap不是同步的也就是非线程安全的在多线程环境下可能出现死循环或数据覆盖等问题。如果需要同步可以使用Collections.synchronizedMap()方法来创建一个同步的HashMap。性能因为Hashtable给方法加了锁所以性能不如HashMap单线程和多线程都是空值存储Hashtable不允许存储null键以及null值HashMap允许键或值为null继承Hashtable继承自Dictionary类一个过时的抽象类。HashMap继承自AbstractMap类。这是 Java集合框架Java Collections Framework的标准实现。迭代器Hashtable迭代器是Enumeration不支持fail-fastHashMap迭代器是Iterator支持fail-fast即在迭代过程中如果其他线程修改集合不会抛出ConcurrentModificationException初始容量与扩容机制HashTable默认初始容量11。 扩容机制newCapacity oldCapacity * 2 1数组链表结构并且不会树化。HashMap默认初始容量16且容量总是 2 的幂次方为了方便通过位运算取模提高计算效率。扩容机制newCapacity oldCapacity * 2当元素数量超过阈值capacity * loadFactor时触发。数组链表红黑树结构链表长度大于8数组长度大于等于64会树化。Hashtable 不推荐使用虽然 Hashtable 是线程安全的但在多线程环境下官方也不推荐使用 Hashtable因为 Hashtable 是给整个方法添加synchronized来实现线程安全的所以它的性能很差。官方推荐在多线程环境下使用线程安全的ConcurrentHashMap来完成数据存储。ConcurrentHashMap锁粒度更细在多线程环境下的性能表现更好。HashMap与ConcurrentHashMap的区别对比项HashMapConcurrentHashMap线程安全不安全安全锁粒度无锁全表操作时可能引发并发问题JDK1.7分段锁JDK1.8CAS synchronized 锁链表/红黑树头节点允许 nullkey 和 value 都允许为 nullkey 和 value都不允许为 null并发性能多线程下可能死循环或数据错乱高并发下性能良好读操作几乎无锁迭代器fail-fast快速失败JDK1.8 迭代器是弱一致性不会抛出 ConcurrentModificationException继承体系继承 AbstractMap继承 AbstractMap实现 ConcurrentMap线程安全性HashMap非线程安全。多线程环境下进行put操作可能导致死循环JDK1.7 头插法扩容时、数据丢失、size 不准确等问题。即使只做读操作若同时有写操作也可能读到脏数据。ConcurrentHashMap线程安全。采用精细化的锁机制允许多个线程并发修改不同的 segment1.7或不同的数组槽位1.8而不需要锁住整个 Map。底层数据结构与锁实现重点版本差异JDK 1.7 的 ConcurrentHashMap结构Segment 数组 HashEntry 数组。Segment 继承自 ReentrantLock每个 Segment 独立加锁相当于将一个 HashMap 拆分成多个子 Map。锁粒度Segment 级别默认 16 个并发度。写操作只锁定对应的 Segment不同 Segment 之间可以并发写入。扩容每个 Segment 内部独立扩容不影响其他 Segment。JDK 1.8 的 ConcurrentHashMap结构Node 数组 链表 / 红黑树与 HashMap 1.8 一致。取消了 Segment 分段锁采用CAS synchronized实现更细粒度的锁。锁粒度数组桶级别。插入时若该位置为空使用CAS插入若不为空则synchronized锁住该桶的头节点链表头或红黑树根节点。不同桶之间完全无锁竞争并发度大幅提升。扩容支持多线程并发扩容每个线程负责一部分数据迁移提升了扩容效率。面试加分点JDK 1.8 的 ConcurrentHashMap 在锁粒度上比 1.7 更细并且利用了 CAS 的无锁操作 synchronized 的优化synchronized 在 JDK 1.6 后引入了偏向锁、轻量级锁等优化性能已经不逊于 ReentrantLock同时解决了 1.7 中遍历时一致性弱的问题。对 null 值的支持HashMap允许一个nullkey 和任意多个nullvalue。ConcurrentHashMapkey 和 value 都不能为 null。为了规避并发场景下的二义性,原因在于设计时为了避免并发情况下的歧义性例如get(key)返回null时无法区分是 key 不存在还是 value 本身为 null以及为了避免在并发环境下因null值引发的非线程安全问题。迭代器与并发修改HashMap的迭代器是fail-fast机制在迭代过程中如果检测到结构被修改除了迭代器自身的remove会抛出ConcurrentModificationException。ConcurrentHashMap的迭代器是弱一致性weakly consistent遍历过程中允许其他线程并发修改迭代器不抛出异常并且尽力保证遍历的线程安全但不保证能立即看到最新的修改。性能对比单线程环境HashMap性能优于ConcurrentHashMap因为ConcurrentHashMap引入了额外的线程安全机制如 CAS 循环、volatile 变量等。多线程环境ConcurrentHashMap远优于Collections.synchronizedMap(new HashMap())全表锁在高并发读写场景下表现优秀。