Hashmap在大厂面试中属于必考的内容,今天花了一个下午看了它的源码,不得不说,真的很经典。就看源码这个问题来说吧,我和我的好基友又产生了隔阂。我认为看hashmap的源码是有必要的,它的底层有很多的优秀的数据结构的应用,比如说红黑树、哈希、数组+链表。虽然以前我就从很多网上经典的面经,了解它的一些内容,但总归只是流于表面,认为自己看的懂就行了,其实这也是学习上一个很大的误区吧。一定要先自己尝试的去IDEA中认真地从头到尾的阅读源码,有自己的那部分理解并记录下来,并且源码一般都有相应的英文注释,同时结合网上一些优秀的博客(开始是想从Java编程思想这本书来找到权威的内容,尴尬的是书中只是一笔带过)进行全面的梳理。
HashMap的定义:
Implementation based on a hash table. (Use this instead of hashtable.) Provides constant-time performance for inserting and locating pairs. Performance can be adjusted via constructors that allow you to set the capacity and load factor of the hash table.
是以哈希数据结构实现的,用来作为hashtable的代替品,在插入和定位元素时表现的很好,通过设置容量和装载因子来调节大小。
HashMap的参数分析:
定义初始的容量大小,必须是2的倍数,默认的长度为16
1static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
定义最大的容量大小,必须是2的29次方
1static final int MAXIMUM_CAPACITY = 1 << 30;
装载因子默认为0.75
1static final float DEFAULT_LOAD_FACTOR = 0.75f;
定义阈值为8,当链表的长度为大于这个阈值时,链表将会转化为红黑树。
1static final int TREEIFY_THRESHOLD = 8;
存储元素的数据,必须是2的倍数
1transient Node<K,V>[] table;
临界值,当实际大小大于临界值时会进行扩容
threshold= tableSizeFor(initialCapacity)
1int threshold;
HashMap的数据结构
Node
1static class Node<K,V> implements Map.Entry<K,V> {
2final int hash;
3final K key;
4V value;
5Node<K,V> next;
6
7 Node(int hash, K key, V value, Node<K,V> next) {
8 this.hash = hash;
9 this.key = key;
10 this.value = value;
11 this.next = next;
12 }
TreeNode
1static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
2 TreeNode<K,V> parent; // red-black tree links
3 TreeNode<K,V> left;
4 TreeNode<K,V> right;
5 TreeNode<K,V> prev; // needed to unlink next upon deletion
6 boolean red;
7 TreeNode(int hash, K key, V val, Node<K,V> next) {
8 super(hash, key, val, next);
9 }
10
11 /**
12 * Returns root of tree containing this node.
13 */
14 final TreeNode<K,V> root() {
15 for (TreeNode<K,V> r = this, p;;) {
16 if ((p = r.parent) == null)
17 return r;
18 r = p;
19 }
20 }
构造方法
1public HashMap(int initialCapacity, float loadFactor) {
2if (initialCapacity < 0)
3throw new IllegalArgumentException("Illegal initial capacity: " +
4initialCapacity);
5if (initialCapacity > MAXIMUM_CAPACITY)
6initialCapacity = MAXIMUM_CAPACITY;
7if (loadFactor <= 0 || Float.isNaN(loadFactor))
8throw new IllegalArgumentException("Illegal load factor: " +
9loadFactor);
10this.loadFactor = loadFactor;
11this.threshold = tableSizeFor(initialCapacity);
12}
tableSizeFor方法
1static final int tableSizeFor(int cap) {
2int n = cap - 1;
3n |= n >>> 1;
4n |= n >>> 2;
5n |= n >>> 4;
6n |= n >>> 8;
7n |= n >>> 16;
8return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
9}
Hashmap扩容方法:
下面的代码是JDK1.8中源码,对1.7的扩容方法进行了改进,解决了在多线程下扩容出现死循环的情况,采用的方法是当链表的长度大于8时,就转化成为红黑树和使用尾插入数据。
1final Node<K,V>[] resize() {
2 Node<K,V>[] oldTab = table;
3 int oldCap = (oldTab == null) ? 0 : oldTab.length;
4 int oldThr = threshold;
5 int newCap, newThr = 0;
6 if (oldCap > 0) {
7 if (oldCap >= MAXIMUM_CAPACITY) {
8 threshold = Integer.MAX_VALUE;
9 return oldTab;
10 }
11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
12 oldCap >= DEFAULT_INITIAL_CAPACITY)
13 newThr = oldThr << 1; // double threshold
14 }
15 else if (oldThr > 0) // initial capacity was placed in threshold
16 newCap = oldThr;
17 else { // zero initial threshold signifies using defaults
18 newCap = DEFAULT_INITIAL_CAPACITY;
19 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
20 }
21 if (newThr == 0) {
22 float ft = (float)newCap * loadFactor;
23 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
24 (int)ft : Integer.MAX_VALUE);
25 }
26 threshold = newThr;
27 @SuppressWarnings({"rawtypes","unchecked"})
28 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
29 table = newTab;
30 if (oldTab != null) {
31 for (int j = 0; j < oldCap; ++j) {
32 Node<K,V> e;
33 if ((e = oldTab[j]) != null) {
34 oldTab[j] = null;
35 if (e.next == null)
36 newTab[e.hash & (newCap - 1)] = e;
37 else if (e instanceof TreeNode)
38 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
39 else { // preserve order
40 Node<K,V> loHead = null, loTail = null;
41 Node<K,V> hiHead = null, hiTail = null;
42 Node<K,V> next;
43 do {
44 next = e.next;
45 if ((e.hash & oldCap) == 0) {
46 if (loTail == null)
47 loHead = e;
48 else
49 loTail.next = e;
50 loTail = e;
51 }
52 else {
53 if (hiTail == null)
54 hiHead = e;
55 else
56 hiTail.next = e;
57 hiTail = e;
58 }
59 } while ((e = next) != null);
60 if (loTail != null) {
61 loTail.next = null;
62 newTab[j] = loHead;
63 }
64 if (hiTail != null) {
65 hiTail.next = null;
66 newTab[j + oldCap] = hiHead;
67 }
68 }
69 }
70 }
71 }
72 return newTab;
73}
线程相关,为啥线程不安全
在并发编程中使用HashMap可能导致程序死循环。在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。例如,执行以下代码会引起死循环。
1final HashMap<String, String> map = new HashMap<String, String>(2);
2 Thread t = new Thread(new Runnable() {
3 @Override
4 public void run() {
5 for (int i = 0; i < 10000; i++) {
6 new Thread(new Runnable() {
7 @Override
8 public void run() {
9 map.put(UUID.randomUUID().toString(), “”);
10}
11}, “ftf” + i).start();
12}
13}
14}, “ftf”);
15t.start();
16t.join();
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
为什么不用hashtable?
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
ConcurrentHashMap的实现原理,分段锁怎么做的
ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel等几个
参数来初始化segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的。
ConcurrentHashMap是线程安全且高效的HashMap。HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的
线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么
当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并
发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。