【JDK】JDK源码分析-HashMap(2)
前文「JDK源码分析-HashMap(1)」分析了 HashMap 的内部结构和主要方法的实现原理。但是,面试中通常还会问到很多其他的问题,本文简要分析下常见的一些问题。
这里再贴一下 HashMap 内部的结构图(JDK 1.8):
FAQ
Q1: HashMap 是否线程安全?为什么?
首先 HashMap 是线程不安全的。这一点很多人应该都了解,HashMap 源码中也有说明。但是为什么说不安全?体现在哪里呢?下面通过两个例子简要进行分析(可能不够全面,仅做参考)。
case 1:
线程 T1 执行 put / remove 等结构性修改(structural modification)的操作;线程 T2 执行遍历操作,这种情况下会抛出 ConcurrentModificationException.
示例代码(以 put 为例):
private static void test() {
Map<Integer, Integer> map = new HashMap<>(); Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
map.put(i, i);
}
}); Thread t2 = new Thread(() -> {
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
System.out.println(entry);
}
}); t1.start();
t2.start();
}
// 执行结果:
// 抛出 java.util.ConcurrentModificationException
原因在这里:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
HashMap 的迭代器和集合视图中,都会对该值进行比较。目的是判断是否有其他线程正在对该 HashMap 进行结构性修改,若有则抛会出异常。
PS: 细心阅读 HashMap 源码的话可以发现,结构性修改的方法中都会有如下一行代码:
++modCount;
该值就是用来记录结构性修改的次数。
case 2:
线程 T1 和 T2 同时执行 put / remove 等结构性修改(structural modification)的操作。以 put 方法为例分析,会发生元素覆盖。
示例代码:
private static void test() throws InterruptedException {
Map<Integer, Integer> map = new HashMap<>(); Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
map.put(i, i);
}
}); Thread t2 = new Thread(() -> {
for (int i = 5000; i < 10000; i++) {
map.put(i, i);
}
}); t1.start();
t2.start(); TimeUnit.SECONDS.sleep(20);
System.out.println(map);
System.out.println("size: " + map.size());
}
// 输出结果:
// {8192=8192, 8193=8193, 8194=8194, 8195=8195, ...
// size: 9666
// PS: 这是某一次的结果,多次测试的具体结果可能不同,但基本都没有 size=10000 的情况。
这里问题出在 put 方法上,通过前文分析 HashMap 中 put 方法的内部实现原理可以找出原因,这里不再赘述。
这里再说一句,HashMap 的设计就是为了单线程下的高效率,了解线程不安全是为了加深对它的理解,知道在哪些情况不适合使用,若有线程安全的需求,可以考虑使用 ConcurrentHashMap。
Q2: 链表和红黑树的转换阈值为什么是 8 和 6 ?
首先分析下为什么会有链表和红黑树。理想情况下,HashMap 中每个 bin 所在位置只有一个节点,这样查询效率最高,为 O(1)。而拉出一个链表、或者把链表再转为红黑树,是在散列冲突比较严重时的一种应对措施,目的是为了让 HashMap 在极端情况下仍然能够保持较高的效率。
至于为什么是 8,HashMap 的部分 Implementation notes 如下:
/* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
*/
由于 TreeNode 的大小是普通节点(Node)的两倍,因此只有当 bin 中包含足够多(即树化的阈值 TREEIFY_THRESHOLD)的节点时才会转为 TreeNode;而当 bin 中节点减少时(删除节点或扩容),又会把红黑树再转为链表。
hashCode 均匀分布时,TreeNode 用到的机会很小。理想情况下,在随机分布的 hashCode 下,bin 中节点的分布遵循泊松分布,并列出了几个数据,可以看到一个 bin 中链表长度达到 8 的概率(0.00000006)不足千万分之一,因此将转换的阈值设为 8.
两个转换阈值及其说明如下:
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8; /**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
至于将红黑树转为链表的阈值为 6,网上有说法是为了避免频繁转换。比如,若红黑树转为链表的阈值也是 8,如果一个 HashMap 不停地进行插入和删除元素,链表的个数一直在 8 左右,这种情况会频繁地进行树和链表的相互转换,效率很低。
这样解释似乎也有些道理,各位可以去探索。
Q3: 为什么负载因子是 0.75?
JDK 1.7 中的相关描述:
/* As a general rule, the default load factor (.75) offers a good tradeoff
* between time and space costs. Higher values decrease the space overhead
* but increase the lookup cost (reflected in most of the operations of the
* <tt>HashMap</tt> class, including <tt>get</tt> and <tt>put</tt>).
*/
下面文章也对此做了分析:
HashMap的loadFactor为什么是0.75?
也许这个问题没必要那么深究,简单来讲,其实就是时间和空间的 tradeoff.
Q4: 为什么容量是 2 的次幂?
位运算 n & (length - 1) 和取余运算 n % length 效果是一样的。但是在计算机中,位运算的效率却远高于取余运算。因此,这样做是为了提高运算效率。
Q5: 一般用什么类型的元素作为 Key?为什么?
一般用 String、Integer 等,它们是不可变的(Immutable),作为不可变类天生是线程安全的。而且重写了 hashCode 方法和 equals 方法。
Q6: 衡量 hash 算法的好坏?String 类的 hashCode 实现?
hash 方法的设计可以有很多。虽然散列值越均匀越好,但 HashMap 首要目的是追求快,因此 hash 算法的设计要尽可能地快。String 类的 hashCode 方法如下:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
PS: 上述问题是本人从网上搜索后整理和思考的结果,仅做参考,并不一定完全准确(要持有怀疑态度)。有关 HashMap 的问题可能还有很多,这里不再一一列举。
参考链接:
https://www.jianshu.com/p/7af5bb1b57e2
相关阅读:
Stay hungry, stay foolish.
PS: 本文首发于微信公众号【WriteOnRead】。
【JDK】JDK源码分析-HashMap(2)的更多相关文章
- JDK Collection 源码分析(2)—— List
JDK List源码分析 List接口定义了有序集合(序列).在Collection的基础上,增加了可以通过下标索引访问,以及线性查找等功能. 整体类结构 1.AbstractList 该类作为L ...
- JDK AtomicInteger 源码分析
@(JDK)[AtomicInteger] JDK AtomicInteger 源码分析 Unsafe 实例化 Unsafe在创建实例的时候,不能仅仅通过new Unsafe()或者Unsafe.ge ...
- 【JDK】JDK源码分析-HashMap(1)
概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...
- 设计模式(十八)——观察者模式(JDK Observable源码分析)
1 天气预报项目需求,具体要求如下: 1) 气象站可以将每天测量到的温度,湿度,气压等等以公告的形式发布出去(比如发布到自己的网站或第三方). 2) 需要设计开放型 API,便于其他第三方也能接入气象 ...
- JAVA源码分析-HashMap源码分析(二)
本文继续分析HashMap的源码.本文的重点是resize()方法和HashMap中其他的一些方法,希望各位提出宝贵的意见. 话不多说,咱们上源码. final Node<K,V>[] r ...
- JDK源码分析 – HashMap
HashMap类的申明 HashMap的定义如下: public class HashMap<K,V> extends AbstractMap<K,V> implements ...
- JDK源码分析-HashMap
一.HashMap的内部属性 1.1 成员变量 1.1.1 size: HashMap包含的KV键值对的数量,也就是我们通常调用Map.size()方法的返回值 public int size() { ...
- JAVA源码分析-HashMap源码分析(一)
一直以来,HashMap就是Java面试过程中的常客,不管是刚毕业的,还是工作了好多年的同学,在Java面试过程中,经常会被问到HashMap相关的一些问题,而且每次面试都被问到一些自己平时没有注意的 ...
- 源码分析--HashMap(JDK1.8)
在JDK1.8中对HashMap的底层实现做了修改.本篇对HashMap源码从核心成员变量到常用方法进行分析. HashMap数据结构如下: 先看成员变量: 1.底层存放数据的是Node<K,V ...
随机推荐
- The Mean of the Sample Mean|Standard Deviation of the Sample Mean|SE
7.2 The Mean and Standard Deviation of the Sample Mean Recall that the mean of a variable is denote ...
- svn使用3 (转)
接下来,试试用TortoiseSVN修改文件,添加文件,删除文件,以及如何解决冲突等. 添加文件 在检出的工作副本中添加一个Readme.txt文本文件,这时候这个文本文件会显示为没有版本控制的状态, ...
- firefox45版本与seleniumIDE
firefox45版本与seleniumIDE https://blog.csdn.net/seanlyly/article/details/80203896 seleniumIDE与firefox版 ...
- JavaScript 事件代理
转自:http://www.cnblogs.com/silence516/archive/2009/09/03/delegateEvent.html 如果你想给网页添加点JavaScript的交互性, ...
- linux中用户和用户组的概念以及查看命令
Linux 是多用户多任务操作系统,换句话说,Linux 系统支持多个用户在同一时间内登陆,不同用户可以执行不同的任务,并且互不影响. 例如,某台 Linux 服务器上有 4 个用户,分别是 root ...
- JAVA递归、非递归遍历二叉树
前序遍历:1.访问根节点 2.前序遍历左子树 3.前序遍历右子树 中序遍历:1.中序遍历左子树 2.访问根节点 3.中序遍历右子树 后序遍历:1.后序遍历左子树 2.后序遍历右子树 3.访问根节点-- ...
- 深入理解Javascript中的valueOf与toString
基本上,javascript中所有数据类型都拥有valueOf和toString这两个方法,null除外.它们俩解决javascript值运算与显示的问题,本文将详细介绍,有需要的朋友可以参考下. t ...
- python标准库-array 模块
原文地址:http://www.bugingcode.com/blog/python_module_array.html array 模块是python中实现的一种高效的数组存储类型.它和list相似 ...
- 使用Navicat for Oracle工具连接oracle出错:ORA-12737
今天上网的时候偶然发现了一款oracle的客户端的图形化管理和开发工具,当看到这个界面的时候,感觉很舒服,便上网搜了一下这个工具,看百度百科之后感觉很出乎我的意料,这个产品对于许多的数据库竟都有支持, ...
- 查漏补缺:QT入门
1.什么世QT Qt是一个跨平台的C++图形用户界面应用程序框架,为应用程序开发者提供建立艺术级图形界面所需的所有功能.它是完全面向对象的,容易扩展,并且允许真正的组建编程. 2.支持平台 Windo ...