1.HashMap 概述
概述
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值(多个)和 null 键(一个)。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)具有很快的访问速度,但遍历顺序却是不确定的
HashMap线程安全吗?
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap
2.HashMap 的实现原理?
HashMap 基于 Hash 算法实现的
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值
3.HashMap的数据存储结构
从结构实现来讲,HashMap是:数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的
- HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,以此来解决Hash冲突的问题
- Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
为什么引入红黑树
因为即使负载因子和Hash算法设计的再合理,也会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。所以引入红黑树是为了增加HashMap的性能,提高HashMap对插入、删除、查找的效率。
4.数据底层具体存储的是什么?这样的存储方式有什么优点呢?
数据底层存储对象
HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象
优点
HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。链地址法,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。例如程序执行下面代码:
map.put("1","A");
系统将调用”1”这个key的hashCode()方法得到其hashCode 值(该方法适用于每个Java对象),然后再通过Hash算法的后两步运算(高位运算和取模运算)来定位该键值对的存储位置,有时两个key会定位到相同的位置,则表示发生了Hash碰撞。
5.HashMap扩容机制
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。
int threshold; // 所能容纳的key-value对极限
int modCount; //记录HashMap内部结构发生变化的次数
int size; //HashMap中键值对数量
static final float DEFAULT_LOAD_FACTOR = 0.75f //负载因子为0.75
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 //Node[] table的初始化长度为16
扩容机制
扩容就是当hashmap中的键值对达到阈值(负载因子*初始长度)时将原数组扩大为原来的两倍,即容量达到最大容量的75%时(这个百分比由负载因子决定)。
6.put方法的流程
7.HashMap为什么要保证hashmap的容量为2的幂次方?
- 为了减小哈希冲突。因为当存在哈希冲突的时候,存储entry的时,会出现一个位置上存在多个entry,造成读写速度的降低;
- 使用2的幂次方的容量,存储的时候,会根据key的哈希值跟hashmap的(容量-1)进行位运算,而当(容量-1)的二进制数全为1的时候,存储位置的计算结果是哈希冲突最小的。
8.为什么HashMap中String、Integer这样的包装类适合作为Key?
- String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
- 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
- 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况
9.如果Object作为HashMap的Key,应该怎么办呢?
应该重写hashCode()和equals()方法
- 重写hashCode()方法:是因为需要计算存储数据的存储位置
- 重写equals()方法:目的是为了保证key在哈希表中的唯一性
10.线程安全的Map都有哪些?性能最好的是哪个?
- 线程安全的有HashTable、ConcurrentHashMap、SynchronizedMap
- 性能最好的是ConcurrentHashMap
11.怎么按添加顺序存储元素?怎么按A-Z自然顺序存储元素?怎么自定义排序?
- 按添加顺序:LinkedHashMap
- 按自然顺序:TreeMap
- 自定义排序:TreeMap(Comparetor c)
12.HashMap 和 ConcurrentHashMap 的区别
- ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好
- 而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
- HashMap的键值对允许有null,但是ConCurrentHashMap都不允许
关于CAS
-
CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
-
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
13.ConcurrentHashMap总结
- 数据结构
取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构 - 保证线程安全机制
JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全 - 锁的粒度
原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node) - 链表转化为红黑树
定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。 - 查询时间复杂度
从原来的遍历链表O(n),变成遍历红黑树O(logN)。
14.HashMap小结
- 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容
- 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊
- HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap
- JDK1.8引入红黑树大程度优化了HashMap的性能
- HashMap的性能提升仅仅是JDK1.8的冰山一角