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() 的相关规定:
- 如果两个对象相等,则 hashcode ⼀定也是相同的
- 两个对象相等,对两个 equals() ⽅法返回 true
- 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
- 综上, equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖
- 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,但是版本号不相等,所以这一次更新失败。