直接拿捏✨HashMap和ConcurrentHashMap
https://blog.csdn.net/qq_45643467/article/details/133184856?spm=1001.2014.3001.5501
1🌟HashMap结构(jdk8)
1 HashMap 特点
- 存储无序的。
- 键和值位置都可以是 null,但是键位置只能存在一个 null。
- 键位置是唯一的,是底层的数据结构控制的。
- jdk1.8 前数据结构是链表+数组,jdk1.8 之后是链表+数组+红黑树。
- 阈值(边界值)> 8 并且数组长度大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。
- 当链表的值小于 6 则会从红黑树转回链表
- table 用来初始化(必须是二的n次幂)
- 初始大小16,加载因子0.75(根据空间和时间,通过泊松分布算法得到的一个折中的值),扩容 resize() 二倍(数组的copy)
- h = key.hashCode() ^ (h >>> 16)🌟使得HashMap中的key的hash值尽可能地均匀分布在整个数组中,从而减少哈希冲突的概率,提高HashMap的性能
key.hashCode()
:获取key对象的哈希值;h >>> 16
:将哈希值右移16位,相当于将其除以2的16次方,也就是将其压缩到一个较小的范围内;h = key.hashCode() ^ (h >>> 16)
:将压缩后的哈希值与原始哈希值进行异或运算,得到最终的哈希值。
2 🌟核心,HashMap的put过程
- 先通过 hash 值计算出 key 映射到哪个桶---->通过 tab=[(n - 1) & hash] 公式计算;
- 如果桶上没有碰撞冲突,则直接插入;
- 如果出现碰撞冲突了,则需要处理冲突:
- 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
- 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
- 如果桶中存在重复的键,则为该键替换新值 value;
- 如果 size 大于阈值 threshold,则进行扩容;
3 为什么HashMap的容量一定要转成2的指数次幂
在 Java 中,HashMap 内部使用了取模运算(%
)来计算散列位置。转化为 2 的指数次幂后,可以使用位运算(&
)来替代取模运算,位运算的效率更高。
4 遍历Map的方式
-
分别遍历Key和Values
-
使用Iterator迭代器迭代
-
通过get方式(不建议使用)
根据阿里开发手册,不建议使用这种方式,因为迭代两次。keySet获取Iterator一次,还有通过get又迭代一次。降低性能
-
jdk8以后使用Map接口中的默认方法
default void forEach(BiConsumer<? super K,? super V> action) BiConsumer接口中的方法: void accept•(T t, U u) 对给定的参数执行此操作。 参数 t - 第一个输入参数 u - 第二个输入参数 public class Demo02 { public static void main(String[] args) { HashMap<String,String> m1 = new HashMap(); m1.put("001", "zhangsan"); m1.put("002", "lisi"); m1.forEach((key,value)->{ System.out.println(key+"---"+value); }); } }
5 HashMap和HashTable区别
HashMap和HashTable都是用于存储键值对的数据结构,但它们之间有几个关键的区别。
- 同步性:HashTable是线程安全的,即多线程并发访问时会自动进行同步处理,而HashMap则不是线程安全的。如果多个线程同时访问HashMap,可能会导致数据不一致或抛出异常。
- 性能:由于HashTable是线程安全的,它在多线程环境下的性能通常比HashMap要低。因为HashTable需要额外的开销来保证线程安全。
- 空值(null):HashMap允许存储空值作为键或值,而HashTable不允许。在HashMap中,可以将null作为键或者值进行存储,但在HashTable中存储null会抛出NullPointerException。
- 继承关系:HashTable是Dictionary类的子类,而HashMap是AbstractMap类的子类。由于Dictionary类是一个古老的类,因此推荐使用HashMap。
- 迭代器:迭代器的失败-fast-fail机制:当对HashMap进行迭代操作时,如果其他线程修改了HashMap的结构(增加或删除元素),会导致ConcurrentModificationException异常。HashTable没有快速失败机制,可以修改并发进行。
- 初始容量和扩容:HashTable默认的初始容量是11,扩容时容量变为原来的两倍加一。HashMap默认的初始容量为16,扩容时容量变为原来的两倍。同时,HashMap可以通过构造函数设置初始容量和负载因子
6 ConcurrentHashMap
ConcurrentHashMap,它是HashMap高并发的版本,线程安全
- ConcurrentHashMap由Segment数组结构和HashEntry数组结构组成;
- Segment是一种可重入锁(ReentrantLock),HashEntry用于存储键值对数据;
- 一个ConcurrentHashMap包含一个由若干个Segment对象组成的数组,每个Segment对象守护整个散列映射表的若干个桶,每个桶是由若干个HashEntry对象链接起来的链表,table是一个由HashEntry对象组成的数组,table数组的每一个数组成员就是散列映射表的一个桶。
1 JDK1.7
数组+链表的方式进行实现,ConcurrentHashMap是使用了锁分段技术来保证线程安全
- 锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
- ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制(synchronized)是一次锁住整个表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个Segment。
- 大数组Segment可以理解为一个数据库,而每个数据库(Segment)中又有很多张表(HashEntry),每个HashEntry中又有很多条数据,这些数据是用链表连接的
- ConcurrentHashMap默认将数组分为16个Segment,诸如get、put、remove等常用操作只锁住当前需要用到的Segment。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,**并发性能的提升是显而易见的【锁的粒度更小】。然后加锁是通过给Segment添加ReentrantLock可重入锁(只有等待当前线程完全执行完成,其他线程才能获取到该锁。)**来实现线程安全的
2 JDK1.8
JDK1.8过后,采用了数组+链表+红黑树的方式优化了ConcurrentHashMap的链表太长导致查询效率降低的问题,具体实现方式为才链表长度大于8,并且数组长度大于等于64时,链表就会升级成红黑树的结构(查询效率就会更高)
1 JDK1.8ConcurrentHashMap是如何实现线程安全的?
在JDK1.8中,ConcurrentHashMap采用了Node+CAS+Synchronized实现线程安全。具体来说,每个Node对象包含了一个HashEntry数组和一个锁对象。当多个线程访问同一个节点时,它们只需要对该节点上的HashEntry数组进行操作,而不需要对整个节点加锁。这样可以提高并发性。
为了确保线程安全,ConcurrentHashMap使用了CAS(Compare And Swap)操作来保证原子性。具体来说,当多个线程同时尝试更新同一个节点时,它们会先获取该节点上的锁对象,然后使用CAS操作将该节点上的HashEntry数组中的某个元素更新为新的值。如果更新成功,则释放锁对象;否则重试直到更新成功为止。这样可以保证多个线程同时更新同一个节点时不会出现数据不一致的问题。
另外,为了保证线程安全,ConcurrentHashMap还使用了Synchronized关键字来对一些关键操作进行加锁。例如,在进行扩容、删除操作时,ConcurrentHashMap会对整个哈希表进行加锁,以防止其他线程在此时修改哈希表中的数据。
1 CAS操作
Compare-And-Swap(CAS)方式。CAS只有在变量值预期值和实际值相等的时候才会更新变量值,并确保在多线程的情况下操作数据时不产生冲突。在CAS的实现中,每个线程都有机会执行自己的操作,并验证操作的正确性,如果操作正确才会进行下一步。这种实现方法相比锁来说减小了锁的开销,大大提高了系统的并发度。
综上,ConcurrentHashMap实现线程安全的方法主要包括分段锁设计和CAS操作,并通过这两种方式保证了多线程操作的正确性和并发效率。
2 介绍CAS
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
7 HashMap和ConcurrentHashMap的区别
1 基本概念不同
- ConcurrentHashMap是一个支持高并发更新与查询的哈希表。在保证安全的前提下,进行检索不需要锁定
- HashMap是基于哈希表的Map接口的实现,此实现提供所有可选的映射操作,并允许使用null值和null键
2 底层数据结构不同
- HashMap的底层数据结构主要是:数组+链表,确切的说是由链表为元素的数组
- ConcurrentHashMap的底层数据结构是:Segments数组+HashEntry数组+链表
3 线程安全属性不同
- ConcurrentHashMap是线程安全的数组,它采用分段锁保证安全性。容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。ConcurrentHashMap让锁的粒度更精细一些(不会对整个数组进行加锁),并发性能更好。
- HashMap不是线程安全,没有锁机制,在多线程环境下会导致数据覆盖之类的问题,所以在多线程中使用HashMap是会抛出异常的。
4 对整个桶数组的处理方式不同
- ConcurrentHashMap对整个桶数组进行了分段
- HashMap则没有对整个桶数组进行分段。