不是完整详细的介绍,只是一个精简版的思路或提纲,在面试时被问到可以在理解的基础上顺着本提纲展开扩充。
简单介绍 HashMap。
- JAVA非常经典的结构,常用于在内存中存放数据;
- 1.7之前底层是基于 数组 + 链表 组成的;
- 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
- put操作:判断当前数组是否需要初始化;如果 key 为空,则 put 一个空值进去;根据 key 计算出 hashcode;根据计算出的 hashcode 定位出所在桶;如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值;如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。
1.8 做了哪些优化?
- 1.7中当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N);
- 所以引入了红黑树;put时判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
有哪些线程安全方面的问题?
HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。
介绍一下ConcurrentHashMap
1.7中:
- ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock;
- HashEntry 中的 value 使用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁。
- get时Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。
- 并发时只在当前segment上上锁,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。
1.8中:
- 使用CAS加synchronized代替ReentrantLock,不再使用Segment;
- 存储使用了红黑树;