背景

HashMap 的相关问题在校招面试中十分常见, 作为新人, HashMap 的各个问题应该要理解的十分透彻才行. 此外, ConcurrentHashMap, Hashtable 也是经常与 HashMap 一同被问, 下文中都有介绍.

HashMap 原理

1. 底层数据结构

HashMap 在 JDK1.8 之前底层使用的是数组+链表的拉链式结构; 在 JDK1.8 之后引入了红黑树, 当链表长度大于阈值的时候就会将这个链表转化为红黑树.

2. JDK1.8 中 HashMap 的改动

如上面所说, JDK1.8 中对 HashMap 做了一些改动, 在 JDK1.8 之前链表的插入使用的是头插法, 作者认为刚刚插入的数据被查询的可能性比较大, 头插法在多线程 resize 的时候可能会产生循环链表. JDK1.8 之后改为了尾插法, 在扩容的时候会保持链表元素原本的顺序, 避免了链表成环的问题, 但是改完以后 HashMap 依然不能支持并发场景. (不过 HashMap 本来也不是为多线程而生的呀)

3. 链表和红黑树的转化

当链表长度大于阈值的时候, 并且哈希桶的数量大于 64 (MIN_TREEIFY_CAPACITY = 64), 就会将这个链表转化为红黑树, 链表转化为红黑树的默认阈值为 8, 如果红黑树的节点个数减少到一定程度也会转化为链表, 这是出于时间和空间的折中方案, 默认会在节点个数减少到 6 的时候进行转化.

4. 默认红黑树转化阈值的选择

上面所讲的阈值为什么选择 8 和 6 呢? 根据泊松分布, 在负载因子为 0.75 (HashMap 的默认值) 的时候, 单个 hash 槽内元素个数为 8 的概率小于百万分之一, 所以将 7 作为一个分水岭, 等于 7 的时候不进行转化, 大于等于 8 时转化为红黑树, 小于等于 6 的时候再转化为链表.

5. hash值的计算

通过阅读源码, 我们可以发现它是通过 (h = key.hashCode()) ^ (h >>> 16) 来计算 hash 值, 混合了 key 哈希值的高 16 位和低 16 位.

6. 扩容机制

HashMap 的默认容量 (其实就是拉链式中数组的长度) 为 16, 每次扩容都会变为原来的 2 倍, 并保证容量为 2 的幂次, 如果在构造函数或者扩容的时候给定一个不是 2 的幂次的数, 它会自动向上扩展到一个 2 的幂次.

7. 为什么 HashMap 的容量要保证是 2 的幂次?

  • 由于使用拉链式的存储方式, 当 put 一个数据的时候, 需要对数组的长度取模确定数据在数组中的位置, 取模过程相对耗时, 因此需要优化取模运算. 当数组长度为 2 的幂次的时候, hash % len 等价于 hash & (len - 1), 与运算相对取模运算更快.
  • 在满足容量为 2 的幂次的时候, (len - 1) 的所有二进制位都为 1, 这种情况下, 只需要保证 hash 算法的结果是均匀分布的, 那么 HashMap 中各元素一定是均匀分布的.
  • HashMap 中有个字段 threshold, 源码注解中写着 The next size value at which to resize (capacity * load factor), 表示它用来判断下次什么时候扩容的字段. 当数组发生扩容时, 只需要再比较 1 bit 即可确定这个节点是否需要移动, 要么不动, 要么移动原来的数组长度.

8. 为什么 HashMap 的默认容量是 16 呢?

这应该是一个经验值, 要保证容量为 2 的幂次, 并且需要在效率和空间上做一个权衡, 太大浪费空间, 太小需要频繁扩容.

HashMap 与 Hashtable 的区别

集合 线程安全性 效率 默认容量 扩容方式 底层结构 实现方式 是否支持null值 迭代器
HashMap 不安全 16 2n (保证是2的幂次) 数组+链表+红黑树 继承AbstractMap类 Key允许存在一个null, Value可以为null Fail-fast 机制
Hashtable 安全 11 2n+1 数组+链表 继承Dictionary类 Key和Value都不能为null Enumerator

1. 线程安全性和效率

首先 HashMap 本来就不是针对多线程情况而设计的, Hashtable 是遗留类, 它内部使用 synchronzied 来修饰方式, 使得它能够成为一个同步集合, 但这种方式效率比较低.

我们可以通过两种方式来获得同步的 HashMap.

  1. 第一种是使用 Collentions.synchronizedMap(Map<K,V> m) 来将一个非同步 Map 变为同步 Map. 这种方式的原理比较简单, 与 Hashtable 类似, 它会把传入的 map 对象作为 mutex 互斥锁对象, 然后在方法里都加上 synchronized(mutex) 的同步.
  2. 第二种是使用 java.util.concurrent 包下的同步集合 ConcurrentHashMap, 这个集合将在下面详细介绍.

2. 对于 null 的支持和迭代器的差异

/* HashMap 中计算 hash 值的过程 */
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/* Hashtable 中 put 的部分源码 */
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
} // Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; ...
}

首先从源码上看, Hashtable 在 put 值为 null 的 key 或者 value 时候会抛出 NullPointerException, 但是 HashMap 对值为 null 的 key 做了特殊处理. 看似很简单的处理, 那这么处理的内在原因是什么呢?

Hashtable 的迭代器使用了安全失败机制 (fail-safe), 这种机制在遍历元素的时候, 先复制原有集合内容, 在拷贝的集合上进行遍历, 这会使得每次读取到的数据并不一定是最新数据. 如果可以使用 null 值, 将会无法判断对应的 key 是不存在还是为空. ConrrentHashMap 也是同样的道理.

HashMap 则是使用安全失败机制 (fail-fast), 这种机制是指在用迭代器遍历一个集合对象的时候, 如果遍历过程中对集合对象的内容进行了修改, 则会抛出 Concurrent Modification Exception. 通过阅读源码, 我们可以发现这种机制使用了 modCount 变量, 每次遍历下个元素的时候, 都会检查 modCount 变量的值是否发生改变, 如果发生改变就会抛出异常. 我们不能依赖这个异常是否抛出来进行并发控制, 这个异常只建议用于检测并发修改的 bug.

java.util 包下的集合 (除了同步容器: Hashtable, Vector 等) 都是 fail-fast, 而 java.util.concurrent 包下的集合和 java.util 包下的同步集合都是 fail-safe.

ConcurrentHashMap 与 Hashtable 的区别

1. 底层结构

ConcurrentHashMap 的底层结构与 HashMap 类似, 使用了数组+链表+红黑树, 而 Hashtable 使用了数组+链表.

2. 实现线程安全的方式

它们都是线程安全的, 但它们实现线程安全的方式不一样.

  • Hashtable 使用同一个对象锁, 用 synchronized 来保证线程安全.

  • ConcurrentHashMap 在 JDK1.7 中使用分段锁, 对整个数组进行分割来分段, 每把锁只锁定一部分数据, 多线程可以访问不同的数据段. Segment 锁继承了 ReentrantLock, 是一种可重入锁, 获取锁时先尝试自旋获取锁, 达到最大自旋次数后改为阻塞方式获取锁, 保证能够获取成功.



  • ConcurrentHashMap 在 JDK1.8 中不再使用分段 (Segment) 的概念, 直接用 Node 数组+链表+红黑树来实现, 使用 CAS + synchronized 来进行并发控制. sychronized 只锁定当前链表或红黑树的头节点, 只要 hash 不冲突就不会有并发问题.

其他知识点

1. HashMap 与 LinkedHashMap 的区别

LinkedHashMap 继承自 HashMap, 底层结构与 HashMap 一致, 主要区别是 LinkedHashMap 维护了一个双向链表, 记录了插入数据的顺序. LinkedHashMap 十分适合用来实现 LRU 算法, LRU 算法主要利用了双向链表和 HashMap, 这简直就是量身打造, 要是手撕代码题用 LinkedHashMap 简直是作弊, 一般面试官不会让你这么干的

HashMap面试知识点总结的更多相关文章

  1. HashMap面试知识点

    HashMap的工作原理是近年来常见的Java面试题. 几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如 ...

  2. Java---常用基础面试知识点

    综合网上的一点资源,给大家整理了一些Java常用的基础面试知识点,希望能帮助到刚开始学习或正在学习的学员. 1.抽象 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方 ...

  3. 知名互联网公司校招 Java 开发岗面试知识点解析

    天之道,损有余而补不足,是故虚胜实,不足胜有余. 本文作者在一年之内参加过多场面试,应聘岗位均为 Java 开发方向.在不断的面试中,分类总结了 Java 开发岗位面试中的一些知识点. 主要包括以下几 ...

  4. Java 面试知识点解析(一)——基础知识篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  5. Java 面试知识点解析(二)——高并发编程篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  6. Java 面试知识点解析(四)——版本特性篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  7. Java开发岗面试知识点解析

    本文作者参加过多场面试,应聘岗位均为 Java 开发方向.在不断的面试中,分类总结了 Java 开发岗位面试中的一些知识点. 主要包括以下几个部分: Java 基础知识点 Java 常见集合 高并发编 ...

  8. Java 开发岗面试知识点

    本文作者在一年之内参加过多场面试,应聘岗位均为 Java 开发方向.在不断的面试中,分类总结了 Java 开发岗位面试中的一些知识点. 主要包括以下几个部分: Java 基础知识点 Java 常见集合 ...

  9. Java面试知识点汇总

    Java面试知识点汇总 置顶 2019年05月07日 15:36:18 温柔的谢世杰 阅读数 21623 文章标签: 面经java 更多 分类专栏: java 面试 Java面试知识汇总   版权声明 ...

随机推荐

  1. [原创] 在C++中实现打字机效果

    如题. void pout(string str,int t)//随便取的,不要介意,str是待输出字符串,t是每两个字的间隔时间. { ;i<str.length();i++) { cout& ...

  2. PHP函数:debug_backtrace

    debug_backtrace()  - 产生一条 PHP 的回溯跟踪(backtrace). 说明: debug_backtrace ([ int $options = DEBUG_BACKTRAC ...

  3. [JS] 自己弄得个倒计时

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. 归并排序(归并排序求逆序对数)--16--归并排序--Leetcode面试题51.数组中的逆序对

    面试题51. 数组中的逆序对 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对.输入一个数组,求出这个数组中的逆序对的总数. 示例 1: 输入: [7,5,6,4] 输出 ...

  5. MVC-过滤器-权限认证

    过滤器主要基于特性,aop来实现对MVC管道中插入其他处理逻辑.比如,访问网站,需要检查是否已经登陆,若没登陆跳入登陆界面. 样例: 方法注册 执行效果 当不符合认证时: 上面是方法注册特性.还有类注 ...

  6. 21.SpringCloud实战项目-后台题目类型功能(网关、跨域、路由问题一文搞定)

    SpringCloud实战项目全套学习教程连载中 PassJava 学习教程 简介 PassJava-Learning项目是PassJava(佳必过)项目的学习教程.对架构.业务.技术要点进行讲解. ...

  7. JasperReports入门教程(四):多数据源

    JasperReports入门教程(四):多数据源 背景 在报表使用中,一个页面需要打印多个表格,每个表格分别使用不同的数据源是很常见的一个需求.假如我们现在有一个需求如下:需要在一个报表同时打印所有 ...

  8. linux内核第一宏 container_of

    内核第一宏 list_entry()有着内核第一宏的美称,它被设计用来通过结构体成员的指针来返回结构体的指针.现在就让我们通过一步步的分析,来揭开它的神秘面纱,感受内核第一宏设计的精妙之处. 整理分析 ...

  9. 详细的JavaScript知识梳理和经典的一百个例题,让你掌握JavaScript

    这里先做一下JavaScript知识点的梳理,具体的可领取资料 JavaScript语法: js语法.png DOM操作: DOM操作.png 数据类型 面向对象 继承 闭包 插件 作用域 跨域 原型 ...

  10. sqlilabs less18-22 HTTP头的注入

    less18 user-agent的注入 源码分析: check_input对name,password进行了过滤 function check_input($value) { if(!empty($ ...