hashmap-大厂必备

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,Vimplements 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,Vextends 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锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值