本文为【Java集合 合集】初版,后续还会进行优化更新,欢迎大家关注交流~
hello hello~ ,这里是绝命Coding------老白~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹
💥个人主页 :绝命Coding-CSDN博客
💥 所属专栏 :后端技术分享
这里将会不定期更新有关后端、前端的内容,希望大家多多点赞关注收藏💖
更多历史精彩文章(篇幅过多,不一一列出):(简历相关)
求职经验分享(1):一份合格的简历应该如何写?-CSDN博客(推荐)
求职经验分享(2):简历如何优化以及如何应对面试【后端篇】-CSDN博客
(项目亮点相关)
大厂面试官赞不绝口的后端技术亮点【后端项目亮点合集(1):Redis篇】-CSDN博客
大厂面试官赞不绝口的后端技术亮点【后端项目亮点合集(2)】-CSDN博客
(八股文)
大厂面试官问我:Redis处理点赞,如果瞬时涌入大量用户点赞(千万级),应当如何进行处理?【后端八股文一:Redis点赞八股文合集】_java中redis如何实现点赞-CSDN博客大厂面试官问我:布隆过滤器有不能扩容和删除的缺陷,有没有可以替代的数据结构呢?【后端八股文二:布隆过滤器八股文合集】_布隆过滤器不能扩容-CSDN博客
.........
集合部分
Java 中常用的容器有哪些?
常见容器主要包括 Collection 和 Map 两种
Collection 存储着对象的集合
Map 存储着键值对
Collection
Set 无序的,元素不可重复
List 有序的,元素可以重复
Queue 先进先出(FIFO)的队列
Map
有映射关系
ArrayList和Vector区别(问过3)
(Vector可以设置增长空间,超量增为原来2倍,线程安全,对应效率慢)
相同
都实现List接口
有序集合
不同
ArrayList 线程不安全
Vector 线程安全(线程同步)
一个线程 ArrayList 不考虑线程安全,效率高
多个线程 Vector 不需要再自己考虑
数据增长
都有初始容量大小
超量时,Vector默认增长为原来两倍,ArrayList没有规定(代码1.5倍)
设置增长空间
Vector可以,ArrayList不行
总结:Vector增长一倍,ArrayList增长0.5倍
【扩展一下:Vector实现线程安全的原理】
ArrayList
ArrayList 可以使用默认的大小,当元素个数到达一定程度后,会自动扩容。
ArrayList的扩容机制及删除元素后是否会缩容
-
扩容机制: 当ArrayList的当前容量不足以容纳要添加的元素时,需要进行扩容操作。扩容通常涉及以下几个步骤:
- 创建一个新的数组,容量通常是原数组的1.5倍(在Java 7及之前)或2倍(Java 8及之后)。
- 将原数组中的元素逐个复制到新数组中。
- 更新ArrayList的引用,使其指向新数组。
- 原数组将被垃圾回收。
-
缩容机制: ArrayList不会自动缩容。即使从ArrayList中删除了元素,它的容量也不会减少。这是因为如果ArrayList反复进行扩容和缩容,会导致性能损失,因为这会涉及到数组元素的复制。如果在之后添加更多元素,可能需要再次进行扩容,这可能会导致不必要的内存分配和复制操作。
Vector线程安全
vector之所以是线程安全的,是因为官方在可能涉及到线程不安全的操作都进行了synchronized操作,相当于官方帮你加了一把同步锁
public synchronized void insertElementAt(E obj, int index) {
modCount++;
if (index > elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " > " + elementCount);
}
ensureCapacityHelper(elementCount + 1);
System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
elementData[index] = obj;
elementCount++;
}
(增删查)
Vector这样的同步容器的所有公有方法全都是synchronized的,保证同一时间只会又一个线程操作同一个方法,也就是说,我们可以在多线程场景中放心的使用单独这些方法,因为这些方法本身的确是线程安全的
(但是对这些容器的复合操作无法保证其线程安全性。需要客户端通过主动加锁来保证。)
但是他不能控制多个线程同时操作多个方法,也就是说,删除和添加是可以同时进行的,这就产生一个问题
所以如果要对Vector进行复杂操作,同样需要手动加锁,在这个时候,我们就应该使用array list,因为这样可以避免Vector自身重复加锁,增加效率
现在有arraylist和hashmap线程非安全的集合类,有什么方法保证线程安全
使用同步容器类: Java 提供了一些线程安全的容器类,例如 Vector
和 Hashtable
**使用 Collections.synchronizedList
和 Collections.synchronizedMap
ArrayList,Vector, LinkedList
(ArrayList数组,LinkedList链表)
ArrayList,Vector 数组 索引数据快(地址连续) 插入数据慢
Vector用了synchronized方法 线程安全
LinkedList 双向链表 插入速度快(只需要记录本项的前后)(地址是任意的,开辟内存空间的时候不需要连续空间一个连续的地址)
ArrayList查找速度快,LinkedList插入和删除更有优势
实习场景
ArrayList有remove函数,其他没有
Arrays.asList 无法remove和add
原因:
内部类ArrayList继承了AbstractList,但是并没有重写add,remove等方法,所以调用这些方法的时候调用的都是父类AbstractList中的方法。
适用场景
当需要对数据进行对随机访问的时候,选用 ArrayList。
当需要对数据进行多次增加删除修改时,采用 LinkedList。
ArrayList和HashMap应该怎么删除元素
Map和Set有什么区别?
Map 映射关系 其key是Set集合,即key无序且不能重复
Set 无序的,元素不可重复
List和Set有什么区别?
List 有序的,元素可以重复
Set 无序的,元素不可重复
HashMap的总容量一定是2的幂次方,即使通过构造函数传入一个不是2的幂次方的容量,HashMap也会将其扩充至与其最接近的2的幂次方的值;比如传入总容量为10,则HashMap会自动将容量扩充至16
重写 hashCode() 方法和 equals() 方法
put 一个 pair 的时候,先计算 key 的哈希值,检查数组中是否有这个哈希值,如果没有哈希冲突,直接存储这个 pair;
如果出现了哈希冲突,就比较两个 key 的哈希值是否一致;哈希值不一致说明不是同一个 key,进行插入操作;哈希值一致则用 equals 方法进行比较,如果不一致则进行插入操作;如果一致则进行更新操作。
实习时候就遇到一个问题:
改造一个接口的时候,校验表结构(因为本来是不同数据库结构的),但现在需要同个数据库不同表之间进行校验,但是原本代码会出现下面的情况
JDBC src = new JDBC('127.0.0.1',3306);
JDBC dst = new JDBC('127.0.0.1',3306);
map.put(src,'12');
map.put(dst,'123');
这样会出现哈希覆盖的问题,最终map只有一个元素,因为JDBC的hashcode和equals都是只判断了host和port
解决方法:用一个子类继承JDBC(多加一个type变量),重写hashcode和equals,这样就不用改太多代码
Map接口有哪些实现类
HashMap 不需要排序 性能最好的Map实现
LinkedHashMap 需要排序且按插入顺序排序
(双向链表)
TreeMap 需要排序且将key排序
(底层红黑树)
ConcurrentHashMap 保证线程安全 性能好于Hashtable
( put 分段锁/CAS的加锁机制
Hashtable put/get 同步处理 )
描述一下Map put的过程
(默认HashMap的⼤⼩为16,负载因⼦的⼤⼩为0.75)
HashMap 最经典的Map实现
( 数组+链表的形式 )
( 扩容->hashh函数->插入(数组/链表)->扩容 )
- 首次扩容:
判断 数组为空 扩容 - 计算索引:
hash算法 计算索引; - 插入数据:
(当前位置元素)
为空 直接插入数据;
非空且key已存在 直接覆盖其value;
非空且key不存在 数据链到链表末端;
若链表长度达到8 链表转换成红黑树 数据插入树 - 再次扩容
元素个数超过threshold 再次扩容
在 Java 中,HashMap
的扩容策略是将哈希表的容量扩大为原来的两倍 。
当 HashMap
中的元素数量超过负载因子阈值时,会触发扩容操作。扩容的目的是为了减少哈希冲突,提高哈希表的性能。
⽐如JDK 7 的HashMap在扩容时是头插法,在JDK8就变成了尾插法
在JDK7 的HashMap还没有引⼊红⿊树
为什么hashmap的扩容因子是0.75
在HashMap中是怎么判断⼀个元素是否相同的呢
⾸先会⽐较hash值,随后会⽤== 运算符和equals()来判断该元素是否相同
如果只有hash值相同,那说明该元素哈希冲突了,如果hash值和equals() || == 都相同,那说明该元素是同⼀个。
hashmap1.7和1.8的扩容机制的不同
transfer方法的作用是把原table的Node放到新的table中,使用的是头插法,也就是说,新table中链表的顺序和旧列表中是相反的,在HashMap线程不安全的情况下,这种头插法可能会导致环状节点。
存储结构
在1.7版本中,HashMap使用数组+链表的方式实现,即当发生哈希冲突时,会使用链表将冲突的元素串起来。
在1.8版本中,当链表长度超过一定阈值(默认为8)时,链表会转化为红黑树,以提高查找效率
并发性
在1.7版本中,HashMap在多线程环境下存在并发问题,可能导致死循环。
在1.8版本中,HashMap引入了"锁分段"机制,将整个存储空间分成了多个段(默认为16段),每个段独立加锁,可以提高并发性能。
扩容机制
在1.7版本中,HashMap的扩容机制是当元素个数超过容量的75%时进行扩容,扩容后容量会翻倍,把所有元素重新计算一遍位置,为了降低hash冲突。
在1.8版本中,扩容机制进行了优化,当链表长度超过一定阈值(默认为8)且元素个数超过容量的75%时,会进行链表转换为红黑树的操作,并且扩容时不再翻倍,而是以原来容量的两倍进行扩容,扩容后,不会把所有元素重新计算一遍位置。
![[20519dac41bc446692944008f4c29c28.png]]
底层原理
HashMap
内部维护了一个存储桶(bucket)的数组,每个桶是一个链表(或红黑树)。
由于哈希函数可能将不同的键映射到相同的桶,可能会导致哈希冲突。解决冲突的方法是使用链表或红黑树来存储在同一个桶中的多个键值对。
初始状态下,所有的桶都是链表。当同一个桶中的链表长度达到一定阈值(8,默认值)时,链表会转换为红黑树,以提高查找的效率。当链表长度小于等于 6 时,红黑树又会退化为链表。
当 HashMap
中存储的键值对数量超过了容量的 75% 时,会触发扩容操作。扩容会重新计算哈希码,重新分配桶,并将原有的键值对重新分配到新的桶中,以减少哈希冲突。
红黑树的特点
为什么不用AVL树
红黑树和 AVL 树都是一种常用的自平衡二叉搜索树,它们的主要目的是保证树的高度平衡,从而保证搜索、插入和删除等操作的时间复杂度都是 O(log n) 级别的。
红黑树和 AVL 树的主要区别包括以下几点:
- 平衡性的要求不同:
AVL 树要求任何节点的左右子树的高度差(平衡因子)不超过 1,因此它的高度更加严格地控制在 O(log n) 级别,搜索效率更高。而红黑树则只要求黑色节点的数量相等,因此它的高度可以略微超过 O(log n) 级别,但是因为旋转操作的次数更少,所以它的插入和删除操作通常比 AVL 树更快。
- 节点结构不同:
AVL 树的节点需要记录平衡因子,因此它的节点结构比红黑树更加庞大。而红黑树只需要记录颜色信息,因此节点结构相对更为简单,可以节省内存空间。
- 插入和删除操作的实现不同:
AVL 树在插入和删除节点时需要通过旋转操作来保持平衡,因此它的操作相对复杂。而红黑树通过颜色变换和旋转操作来保持平衡,操作相对简单。
可以看出,红黑树和 AVL 树都是自平衡二叉搜索树,它们的实现方式不同,各有优缺点。
红黑树适用于插入、删除操作较多的场景,而 AVL 树适用于搜索操作较多的场景 。而 HashMap 会用于插入和删除较多场景,并且从空间的角度来看的话,使用红黑树比较合适
为什么链表和红黑树的相互转换不会消耗性能
- 转换条件:在Java 8中,当链表长度超过阈值(默认为8)时,链表会转换为红黑树;当红黑树节点数小于阈值(默认为6)时,红黑树会转换回链表。这些转换条件是为了平衡链表和红黑树的性能,并在特定情况下提供更快的操作。
因此,大多数情况下,链表和红黑树的转换发生的并不频繁,对HashMap的整体性能影响有限。
hashmap的扩容机制
HashMap的扩容机制是指在HashMap中的元素数量达到一定阈值时,自动进行数组容量的扩展。这是为了保持HashMap的性能和负载因子(Load Factor)在一个较好的范围内。
(1)扩容时,HashMap会创建一个新的两倍大小的数组 ,并将原有数组中的元素重新分配到新数组的对应位置上。
(2)元素的重新分配是根据它们的哈希值和新数组的容量来确定的。每个键值对会根据其哈希值与新数组的容量进行按位与运算,决定在新数组中的位置。
(3)在重新分配元素的过程中,如果发生哈希冲突(即多个键值对的哈希值对应到了新数组的同一个位置),则采用链表或红黑树等数据结构来解决冲突。
(4)扩容完成后,新数组将取代旧数组成为HashMap的存储结构,原有的数组会被垃圾回收。
线程安全和非线程安全的Set集合
线程安全:
- ConcurrentSkipListSet: 这是一个基于跳表的有序集合,是线程安全的,可以在多线程环境中并发使用。
- CopyOnWriteArraySet: 这是一个基于数组的集合,采用了写时复制机制,每次修改操作会创建一个新的副本,因此可以在多线程环境中并发使用。
- Collections.synchronizedSet: 你可以通过
Collections.synchronizedSet
方法来创建一个线程安全的集合,它会对原始的非线程安全集合进行同步处理,但在高并发环境下可能会有性能问题。
非线程安全:
- HashSet: 基于哈希表实现的集合,不是线程安全的。
- LinkedHashSet: 基于哈希表和链表的有序集合,也不是线程安全的。
- TreeSet: 基于红黑树实现的有序集合,同样不是线程安全的。
写时复制
如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
HashMap有什么特点?
线程不安全
key或value 可以是null
HashMap 的底层数据结构?JDK1.7 和 JDK1.8
JDK1.7:Entry数组 + 链表
缺点: 冲突严重 链表长 查询效率低
(
数组:占用空间连续,寻址容易,查询速度快,但是增删效率低
链表:占用不空间不连续,寻址困难,查询速度慢,但是增删效率高
HashMap结合了两者的优点
)
JDK1.8:Node 数组 + 链表/红黑树(大于8,链表变红黑树)
优化查询时间 链表为O(N) 红黑树O(logN)
线程安全和非线程安全
线程安全指的是在多线程环境下,代码或数据结构可以正确地处理多个线程的并发访问,而不会出现意外的结果或破坏数据的一致性。线程安全的代码可以被多个线程同时调用而不会引发竞态条件(Race Condition)或其他并发问题。
非线程安全指的是在多线程环境下,代码或数据结构无法正确地处理多个线程的并发访问,可能会导致意外的结果或破坏数据的一致性 。非线程安全的代码在多线程环境中需要额外的同步机制来保证正确性,否则可能导致数据竞争、内存泄漏、死锁等并发问题。
HashMap 的实现原理
hash算法
(先正常算出哈希值,然后与高16位做异或运算->增加了随机性,减少碰撞冲突的可能)
发生碰撞时 用链表
元素超过8,用红黑树来替换链表(JDK1.8的)
HashMap为什么线程不安全?
(1.7头插环形链表死循环;1.8尾插法put会覆盖,扩容?)
JDK1.7 头插法插入元素,并发情况下会导致环形链表,产生死循环
JDK1.8 采用尾插发解决上述问题,但是并发下的put操作也会使前一个key被后一个key覆盖
由于HashMap有扩容机制存在,也存在A线程进行扩容后,B线程执行get方法出现失误的情况
/
(1)在两个线程同时尝试扩容HashMap 时,可能将一个链表形成环形的链表,所有的next都不为空,进入死循环;
假设两个线程同时处理相同的一个元素,根据元素的hash值,它们可能会同时决定将该元素放入新桶的相同位置。如果不进行同步操作,它们可能会同时更新该位置的链表头,导致环形链表的形成。
(2)在两个线程同时进行put时可能造成一个线程数据的丢失;
/
- 多个线程 put 不同键值对时,会导致键值对被覆盖,从而丢失键值对数据
- 多个线程 put 键值对,导致实际的键值对数量和 size() 返回的数量对不上,因为 size 变量是一个全局的变量
- 一个线程 get,另一个线程进行扩容,会导致 get 不到键值对,因为有可能刚好这个键值对扩容换了桶
- 在 JDK 1.7 及以前,在多线程下进行扩容,或产生循环引用的问题,从而使得 CPU 100%,不过在 1.8 后没这个问题了
如何得到一个线程安全的Map?
1.用Collections工具类 包装
2.用ConcurrentHashMap
3.不建议使用Hashtable 线程安全 但性能差
ConcurrentHashMap底层原理
ConcurrentHashMap 在 Java 1.7
和 Java 1.8
中的实现方式有所不同,但它们的共同目标都是提供高效的并发更新操作。下面我将分别介绍这两个版本的实现方式。
-
Java 1.7 :在Java 1.7中,ConcurrentHashMap 内部使用一个 Segment 数组来存储数据。每个Segment 对象包含一个 HashEntry 数组,每个 HashEntry 对象就是一个键值对。Segment 对象是可锁定的,每个Segment对象都可以看作是一个独立的 HashMap。在更新数据时,只需要锁定相关 Segment 对象,而不需要锁定整个HashMap,这样就可以实现真正的并发更新。Segment 的数量默认为 16,这意味着 ConcurrentHashMap 最多支持 16 个线程的并发更新。
(分段锁)
-
Java 1.8 :在 Java 1.8 中,ConcurrentHashMap 的实现方式进行了改进。它取消了 Segment 数组,直接使用 Node 数组来存储数据。每个Node对象就是一个键值对。在更新数据时,使用 CAS 操作和 synchronized 来保证并发安全。具体来说,如果更新操作发生在链表的头节点,那么使用 CAS 操作;如果发生在链表的其他位置,或者发生在红黑树节点,那么使用synchronized。这样,ConcurrentHashMap可以支持更多线程的并发更新。
(头结点CAS,其他synchronize)
1.8原因
这是因为在并发环境下,对链表头节点的更新操作比对其他位置的更新操作更频繁。使用CAS操作可以有效地解决并发冲突的问题。
而对于链表的其他位置的更新操作,由于涉及到对节点的删除、插入等操作,相对于头节点的更新操作较少。在这种情况下,使用CAS操作可能会增加复杂性,并发冲突的概率也较低。
使用CAS操作可能无法满足对红黑树结构的保证。在这种情况下,需要使用其他的同步机制,如锁或读写锁,来确保对红黑树的更新操作的正确性和一致性。
高并发下 ReentrantLock 性能比 synchronized 高,那为什么 ConcurrentHashMap 在 JDK 1.7 中要用 ReentrantLock,而 ConcurrentHashMap 在 JDK 1.8 要用 Synchronized
这是一个很好的问题。首先,我们需要明确一点,虽然 ReentrantLock
在某些情况下的性能优于synchronized,但这并不意味着在所有情况下都是这样。
实际上,synchronized
在JDK 1.6 及以后的版本中已经进行了大量的优化,例如偏向锁、轻量级锁等,使得在竞争不激烈的情况下,synchronized 的性能已经非常接近甚至超过 ReentrantLock。
在 JDK 1.7的 ConcurrentHashMap中,使用 ReentrantLock(Segment)是为了实现分段锁的概念,即将数据分成多个段,每个段独立加锁,从而实现真正的并发更新。这种设计在当时是非常先进和高效的。
然而,在 JDK 1.8 的 ConcurrentHashMap
中,为了进一步提高并发性能,引入了更复杂的数据结构(如红黑树)和更高效的并发控制机制(如CAS操作)。在这种情况下,使用 synchronized
比 ReentrantLock
更加简单和高效。首先,synchronized 可以直接与JVM进行交互,无需用户手动释放锁,减少了出错的可能性。
其次,synchronized
在 JDK 1.6及以后的版本中已经进行了大量的优化,性能已经非常接近 ReentrantLock。最后,synchronized 可以与其他 JVM 特性(如偏向锁、轻量级锁、锁消除、锁粗化等)更好地配合,进一步提高性能。
总的来说,选择使用 ReentrantLock
还是 synchronized
,需要根据具体的需求和使用场景来决定。在 JDK 1.8
的 ConcurrentHashMap中,使用 synchronized 是一种更加合理和高效的选择。
HashMap如何实现线程安全?
1.用Hashtable(不推荐)
(key和value不能为null,把整个对象锁了,粒度大,效率低)
Map<String, String> hashtable =` `new``Hashtable<>();
HashTable源码中是使用synchronized
来保证线程安全的,
当一个线程访问HashTable的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。
举个例子,当一个线程使用put方法时,另一个线程不但不可以使用put方法,连get方法都不可以,所以效率很低,基本不会选择它
Hashtable 的 key 和 value 都不允许为 null
(把整个对象都锁了,粒度大)
2.用ConcurrentHashMap
(分段加锁,代码繁琐,效率高)
ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>();
map.put("name","liliheng");
System.out.println(map.get("name"));
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key,V value) {
synchronized (mutex) {return m.put(key,value);}
}
在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例
Mutex在构造时默认赋值为this,即所有方法都用的同一个锁,m就是我们传入的map
优点:
java.util.concurrent包下的ConcurrentHashMap 对hashmap进行拆分使用了新的锁机制 NonfairSync ,
作用域小,效率高
缺点:
代码比较繁琐
ConcurrentHashMap加锁方式
(分段,指定段加锁)
内部细分了若干个小的 HashMap,称之为段(Segment)。默认情况下一个 ConcurrentHashMap 被进一步细分为16 个段 ,既就是锁的并发度。如果需要在 ConcurrentHashMap 中添加一个新的表项,并不是将整个 HashMap 加锁,而是首先根据 hashcode 得到该表项应该存放在哪个段中,然后对该段加锁,并完成 put 操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行
在 JDK 1.7 中,ConcurrentHashMap 使用了分段锁,即将哈希表分成多个段,每个段拥有一个独立的锁。这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段即可,而不是锁整个哈希表
(在JDK7 还是使⽤分段锁的⽅式来)
这种分段锁虽然可以减少锁竞争,但是在高并发场景下,仍然会出现锁竞争,从而导致性能下降,所以在 JDK 1.8 中,对 ConcurrentHashMap 的实现进行了优化,采用 CAS + Synchronized 的机制来保证线程安全
在 JDK 1.8 中,ConcurrentHashMap 会在添加或者删除元素时,首先使用 CAS 操作来尝试修改元素,如果 CAS 失败,则使用 Synchronized 锁住当前的槽,再次尝试调加或者修改,这样锁的粒度减少,锁的竞争也变少,提高了并发性
3.用Collections包装(Collections.synchronizedMap)
(对方法加锁,有效范围大,效率低)
HashMap hashMap=new HashMap();
Map<Object, Object> map = Collections.synchronizedMap(hashMap);
map.put("name","liliheng");
System.out.println(map.get("name"));
在jdk1.8中ConcurrentHashMap利用CAS算法
优点:
Collections.synchronizedMap()对map的一个封装返回一个线程安全的map,
代码简单,
缺点:
但是是对方法进行加锁,锁的有效范围大,效率低
【扩展:HashMap为什么是线程不安全】
ConcurrentHashMap底层原理(
-
分段锁:
ConcurrentHashMap
内部由多个独立的段(Segment)组成,每个段都是一个独立的哈希表。每个段都有自己的锁,不同的线程可以同时访问不同的段,从而提供了更高的并发性能。默认情况下,ConcurrentHashMap
的段数与 CPU 核心数相等,可以通过构造函数参数来进行调整。 -
Hash 分布:
ConcurrentHashMap
使用哈希算法将键映射到不同的段中。具体的映射方式是通过对键的哈希值进行计算,然后根据段的数量取模来确定键所属的段。这样可以将不同的键分布到不同的段中,减少了并发访问时的竞争。 -
锁粒度:
ConcurrentHashMap
的锁粒度是段级别的,即每个段都有自己的锁。这样,在并发访问时,只有访问同一个段的线程需要竞争锁,而不同段之间的访问可以并行进行,提高了并发性能。 -
线程安全操作:
ConcurrentHashMap
的每个段内部使用了类似于HashMap
的数据结构,但是对于并发访问的操作进行了线程安全的处理。在插入、删除、修改等操作时,
ConcurrentHashMap 通过分段锁设计、数组+链表/红黑树的数据结构、CAS 操作和无锁算法等
ConcurrentHashMap并发度
ConcurrentHashMap 的并发度就是 segment 的大小,默认为 16,这意味着最多同时可以有
16 条线程操作 ConcurrentHashMap,这也是 ConcurrentHashMap 对 Hashtable 的最大优势。
ConcurrentHashMap
允许你指定并发度,即同一时间可以有多少线程访问哈希表的不同部分。并发度实际上是哈希表的分段数,
容量(Capacity): ConcurrentHashMap
的容量指的是哈希表中可以容纳的键值对数量。当哈希表中的键值对数量达到容量的阈值时,ConcurrentHashMap
会自动进行扩容操作,以保证哈希表的负载因子在一个可接受的范围内,从而维持较低的碰撞率。容量与扩容操作有关,影响着内存使用和性能。
HashMap怎么判断一个元素是否相同
比较hash值,用==
和equals来判断
只有hash值相同,元素哈希冲突
hash值和equals||``==`都相同,同一个元素
HashMap是如何解决哈希冲突的?
链表
链表长度到达8,变红黑树;缩小,变回链表
扩展
当链表长度过长时,会影响到查询、插入和删除等操作的性能
当HashMap中的元素数量变得较少时,使用红黑树反而会降低性能,因为红黑树比链表需要更多的空间,而且在元素数量较少时,链表的查找、插入和删除操作仍然非常高效。因此,在元素数量较少时,将红黑树转换回链表可以提高性能并避免浪费空间。
说一说HashMap和HashTable的区别
HashMap和HashTable
线程不安全 线程安全
key/value可以是null 不可以(出现空指针异常)
为什么HashTable的键值不可以是null,hashmap就可以
HashMap计算key的hash值时调用单独的方法,在该方法中会判断key是否为null,如果是则返回0,可存在一个为null的key,value值可为null;
而Hashtable中则直接调用key的hashCode()方法,因此如果key为null,则抛出空指针异常
HashMap与ConcurrentHashMap有什么区别?
HashMap 线程不安全(并发操作 ,循环链表,死循环)
ConcurrentHashMap 线程安全(减少锁粒度,减少因为竞争锁而导致的阻塞与冲突,检索操作是不需要锁)
ConcurrentHashMap的key值能否为null
key值不能为null
在ConcurrentHashMap中,每个键值对都被保存在一个Entry对象中,而每个Entry对象都被保存在一个数组中,这个数组被称为Segment。ConcurrentHashMap中的每个Segment都是一个独立的哈希表,它具有自己的锁,因此不同的Segment可以被不同的线程同时访问。
在ConcurrentHashMap中,键值对的键不能为null。这是因为在ConcurrentHashMap中,每个键值对的键都被用来计算其在Segment数组中的索引位置,如果键为null,那么在计算索引位置时会出现NullPointerException异常。
扩展
HashMap对象的key、value值均可为null。
HahTable对象的key、value值均不可为null。
且两者的的key值均不能重复,若添加key相同的键值对,后面的value会自动覆盖前面的value,但不会报错。
HashMap计算key的hash值时调用单独的方法,在该方法中会判断key是否为null,如果是则返回0,可存在一个为null的key,value值可为null;
而Hashtable中则直接调用key的hashCode()方法,因此如果key为null,则抛出空指针异常
后期新的八股文合集文章会继续分享 ,感兴趣的小伙伴可以点个关注~
更多精彩内容以及免费资料请关注公众号:绝命Coding