面试题答案
一键面试1. HashMap扩容机制
- 扩容条件:当HashMap中的元素个数(size)达到容量(capacity)与负载因子(load factor,默认0.75)的乘积时,就会触发扩容。例如,初始容量为16,当元素个数达到16 * 0.75 = 12时,就会进行扩容。
- 扩容过程:扩容时,会创建一个新的更大的数组,新容量为原来的2倍。然后将旧数组中的所有元素重新计算哈希值并放入新数组中。这个过程比较耗时,因为需要重新计算哈希和移动元素。
2. ConcurrentHashMap扩容机制
- JDK 1.7:
- 分段锁机制:采用Segment数组和HashEntry数组实现,Segment继承自ReentrantLock。每个Segment管理一个HashEntry数组。
- 扩容方式:当某个Segment中的元素达到一定阈值时,只对该Segment进行扩容,而不是整个ConcurrentHashMap。这样可以减少扩容时的竞争,提高并发性能。
- JDK 1.8:
- 摒弃分段锁:采用Node数组 + 链表/红黑树结构,利用CAS和synchronized实现线程安全。
- 扩容方式:当元素个数达到容量的0.75倍时触发扩容。扩容时采用了transfer方法,将数据迁移到新的数组。在迁移过程中,会使用多个线程并行进行数据迁移,大大提高了扩容效率。
3. 扩容机制差异导致的性能表现不同
- 高并发读:
- HashMap:本身是非线程安全的,在高并发读场景下,如果没有外部同步机制,可能会出现数据不一致的问题。
- ConcurrentHashMap:无论是JDK 1.7还是1.8,都支持高并发读。在JDK 1.7中,读操作基本无锁(除了涉及到size计算等操作);在JDK 1.8中,读操作通过volatile关键字保证可见性,性能较高。
- 高并发写:
- HashMap:非线程安全,高并发写时可能会出现数据覆盖等问题,性能也会因同步机制的添加而大幅下降。
- ConcurrentHashMap:JDK 1.7通过分段锁,不同Segment的写操作可以并发执行;JDK 1.8采用CAS和synchronized,在写操作时竞争更小,性能优于HashMap。
- 大量数据插入:
- HashMap:扩容时需要重新计算哈希和移动元素,在大量数据插入时,扩容开销较大,性能会受到影响。
- ConcurrentHashMap:JDK 1.8中在大量数据插入时,由于采用多线程并行扩容,相比HashMap单线程扩容,性能更优。
4. 根据业务场景选择合适的集合类
- 非并发场景:如果业务场景是非并发的,HashMap是一个很好的选择,因为它简单高效,没有线程安全带来的额外开销。
- 高并发读多写少:ConcurrentHashMap在这种场景下表现出色,特别是JDK 1.8版本,读操作几乎无锁,性能很高。
- 高并发写多读少:同样可以选择ConcurrentHashMap,JDK 1.8的优化使得写操作的竞争减小,性能较好。如果写操作频率极高且对数据一致性要求不是特别严格,还可以考虑使用CopyOnWriteArrayList(针对List场景)的思想来实现类似集合。
- 对数据一致性要求极高:在高并发场景下,如果对数据一致性要求极高,如银行转账等场景,需要在使用ConcurrentHashMap时结合其他同步机制来保证操作的原子性和一致性。