关于HashMap和ConcurrentHashMap理解

1.HashMap和Hashtable区别

1.线程是否安全:HashMap是非线程安全的,HashTable是线程安全的,因为HashTable内部的方法基本都经过Synchronized修饰(如果要保证线程安全就使用ConcurrentHashMap)
2.效率:因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable基本被淘汰,不要在代码中使⽤它。
3.对Null key 和 Null value 的⽀持:HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException。
4.初始容量⼤⼩和每次扩充容量⼤⼩的不同:① 创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩( HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩。
5.底层数据结构:JDK1.8 以后的 HashMap 在解决哈希冲突时有了较⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。

2.HashMap 和 HashSet区别

  • HashSet 底层就是基于 HashMap 实现的。( HashSet 的源码⾮常⾮常少,因为除了 clone() 、writeObject() 、 readObject() 是 HashSet⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。

2.1 HashSet如何检查重复

  • 当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同
    时也会与其他加⼊的对象的 hashcode 值作⽐᫾,如果没有相符的 hashcode , HashSet 会假设
    对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查
    hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让加⼊操作成功

hashCode() 与 equals() 的相关规定:

  1. 如果两个对象相等,则 hashcode ⼀定也是相同的
  2. 两个对象相等,对两个 equals() ⽅法返回 true
  3. 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
  4. 综上, equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖
  5. hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该
    class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)。
    ==与 equals 的区别
    对于基本类型来说,== ⽐᫾的是值是否相等;
    对于引⽤类型来说,== ⽐᫾的是两个引⽤是否指向同⼀个对象地址(两者在内存中存放的地址
    (堆内存地址)是否指向同⼀个地⽅);
    对于引⽤类型(包括包装类型)来说,equals 如果没有被重写,对⽐它们的地址是否相等;如果
    equals()⽅法被重写(例如 String),则⽐᫾的是地址⾥的内容。

2.2 HashMap的底层实现

2.2.1 JDK1.8之前

  • JDK1.8 之前 HashMap 底层是 数组和链表 结合在⼀起使⽤也就是 链表散列。HashMap 通过
    key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素
    存放的位置(这⾥的 n 指的是数组的⻓度),如果当前位置存在元素的话,就判断该元素与要存
    ⼊的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲
    突。
  • 所谓扰动函数指的就是 HashMap 的 hash ⽅法。使⽤ hash ⽅法也就是扰动函数是为了防⽌⼀
    些实现⽐较差的 hashCode() ⽅法 换句话说使⽤扰动函数之后可以减少碰撞。

2.2.2 JDK1.8 之后

  • 相⽐于之前的版本, JDK1.8 之后在解决哈希冲突时有了᫾⼤的变化,当链表⻓度⼤于阈值(默
    认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组
    扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都⽤到了红⿊树。红⿊树就是为了
解决⼆叉查找树的缺陷,因为⼆叉查找树在某些情况下会退化成⼀个线性结构。

2.3 HashMap 多线程操作导致死循环问题

  • 主要原因在于 并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这
    个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其
    他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。

3.ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现,JDK1.8
    采⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红⿊⼆叉树。 Hashtable 和
    JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是
    HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
  • 实现线程安全的⽅式(重要)① 在 JDK1.7 的时候ConcurrentHashMap (分段锁)
    对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问
    容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经
    摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发
    控制使⽤ synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优
    化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到
    Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable (同⼀把
    ) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其
    他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不
    能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。

两者对比图:
HashTable
在这里插入图片描述
JDK1.7 的 ConcurrentHashMap:
在这里插入图片描述
JDK1.8 的 ConcurrentHashMap:

  • JDK1.8 的 ConcurrentHashMap 不在是 Segment 数组 + HashEntry 数组 + 链表,⽽是 Node 数
    组 + 链表 / 红⿊树
    。不过,Node 只能⽤于链表的情况,红⿊树的情况需要使⽤ TreeNode 。当
    冲突链表达到⼀定⻓度时,链表会转换成红⿊树

3.1 ConcurrentHashMap线程安全的具体实现⽅式/底层具体实现

3.1.1 JDK1.7

⾸先将数据分为⼀段⼀段的存储,然后给每⼀段数据配⼀把锁,当⼀个线程占⽤锁访问其中⼀个
段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock ,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤
于存储键值对数据.

⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组。 Segment 的结构和 HashMap 类似,是
⼀种数组和链表结构,⼀个 Segment 包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表
结构的元素,每个 Segment 守护着⼀个 HashEntry 数组⾥的元素,当对 HashEntry 数组的数
据进⾏修改时,必须⾸先获得对应的 Segment 的锁。

3.1.2 JDK1.8

ConcurrentHashMap 取消了 Segment 分段锁,采⽤ CAS 和 synchronized 来保证并发安全。数
据结构跟 HashMap1.8 的结构类似,数组+链表/红⿊⼆叉树。Java 8 在链表⻓度超过⼀定阈值
(8)时将链表(寻址时间复杂度为 O(N))转换为红⿊树(寻址时间复杂度为 O(log(N)))

synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并
发,效率⼜提升 N 倍。

3.2 ⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

  • HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存
    储 null 值;
  • LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
  • TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排
    序。

3.3 jdk1.7升级到1.8,ConcurrentHashMap的变化

  • 锁方面:由分段锁(Segment继承自ReentrantLock)升级为CAS+synchronized实现;
  • 数据结构层面:将Segment变为了Node,每个Node独立,原来默认的并发度16,变成了每个Node都独立,提高了并发度;
  • hash冲突:1.7中发生hash冲突采用链表存储,1.8中先使用链表存储,后面满足条件后会转换为红黑树来优化查询;
  • 查询复杂度:1.7中链表查询复杂度为O(N),1.8中红黑树优化为O(logN));
    在这里插入图片描述

3.4 链表长度为什么超过8要转为红黑树?

  • 默认的是链表结构,并不是一开始就是红黑树结构,因为链表比红黑数占用的空间较少;hash冲突导致链表长度真正达到8的概率极小,约为一千万分之一,同时也考虑到红黑树查询比链表快;

4 .CAS机制

  • 从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
  • 真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
  • 假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。
    -在这里插入图片描述
    这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。
    在这里插入图片描述
    随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值