HashMap也算是面试经常问的问题了,常见的HashMap数据结构、碰撞、线程不安全以及解决策略都是Java程序员必须熟悉的内容,这里简单归纳一下啦。
一、HashMap的数据结构
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是为了解决哈希冲突而存在的。当链表长度大于8时,链表会转换成红黑树(JDK 1.8),当小于6时,又会退化为链表。
HashMap的特点:
(1)HashMap是非安全的(非synchronized)。
(2)继承了数组的线性查找和链表的寻址修改的特性,所以查找元素和修改元素的效率都比较快。
(3)HashMap的 key和value都可以是null,而Hashtable则不能。(原因是Hashtable使用equal()方法会产生空指针异常,而HashMap经过API处理过的,不会出现在这种情况)
二、HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法存储和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode值,然后找到bucket位置来存储键值对(Entry对象);当使用get()方法获取对象时,再通过键对象的equals()方法找到正确的键值对(Entry对象),然后返回值对象。
在使用put()方法时,如果键对象已经存在,则用新值覆盖旧值,否则,将键值对对象(Entry对象)存储在链表尾部。
三、解决碰撞冲突(了解)
01、开放寻址法
思路:以某种方式寻找数组中空余的结点位置
实现:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p1为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi。
02、链地址法
思路:引入链表
实现:将数组中每一个实体存储为链表结构,如果发生碰撞,则把旧结点指针指向新链表结点,此时查询碰撞结点只需要遍历该链表即可。
四、当两个对象的HashCode相同会发生什么?
因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表(LinkedList)存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表(LinkedList)中。
五、如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
(当HashMap中的元素个数(size)> 负载因子(loadFactor) * 当前容量(capacity)即会发生扩容。默认情况下,负载因子为0.75,初始容量为16。)
六、重新调整HashMap大小存在什么问题?
当重新调整HashMap时,可能存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小,他们会试着同时调整大小,在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(但是这会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空),那么就死循环了。
七、如何解决HashMap多线程不安全问题
1、put的时候导致的多线程数据不一致
2、put操作可能因为resize而引起死循环(cpu 100%)
所以并发情况下不能使用HashMap。那该怎么办呢?可以使用HashTable。HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。那怎么做才能既保证线程安全,又提高效率呢?可以使用ConcurrentHashMap,因为ConcurrentHashMap使用了锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
八、ConcurrentHashMap的结构
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一个可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。