面试题答案
一键面试在多线程环境中使用Java HashMap出现的问题及原因
- 数据丢失或覆盖:
- 原因:在多线程环境下,当多个线程同时对HashMap进行put操作时,可能会出现数据丢失或覆盖的情况。例如,两个线程同时计算出相同的哈希值,并且在put操作时,第二个线程可能会覆盖第一个线程刚刚插入的数据。从源码角度看,HashMap的put方法没有任何线程同步机制。在JDK 1.8之前,put操作在处理哈希冲突时采用链表法。假设线程A和线程B同时往HashMap中put数据,且哈希冲突,线程A可能在链表头插入节点,线程B也在链表头插入节点,由于没有同步,可能导致数据丢失。在JDK 1.8及之后,引入了红黑树,当链表长度达到一定阈值会转换为红黑树,但同样没有线程同步机制,在多线程插入时仍可能出现数据不一致问题。
- 死循环(JDK 1.7及之前):
- 原因:在JDK 1.7及之前,HashMap的resize方法在多线程环境下可能导致死循环。当多个线程同时检测到需要扩容并执行resize操作时,由于采用头插法,链表顺序会被反转。例如,线程A和线程B同时进行resize操作,线程A可能在处理链表节点时,线程B也在处理相同链表节点,导致链表形成环,从而在后续的get操作时陷入死循环。
- 线程安全问题:
- 原因:HashMap不是线程安全的,其方法没有同步修饰符。例如,当一个线程在遍历HashMap时,另一个线程对其进行修改(如remove或put操作),可能会抛出
ConcurrentModificationException
异常,因为遍历过程中使用的迭代器依赖于modCount变量,当其他线程修改HashMap时,modCount会发生变化,迭代器检测到这种变化就会抛出异常。
- 原因:HashMap不是线程安全的,其方法没有同步修饰符。例如,当一个线程在遍历HashMap时,另一个线程对其进行修改(如remove或put操作),可能会抛出
除ConcurrentHashMap外的解决方案及优缺点
- 使用HashTable:
- 优点:HashTable是线程安全的,其大部分方法(如put、get等)都被
synchronized
关键字修饰,因此在多线程环境下可以安全使用,不会出现数据不一致等问题。 - 缺点:性能较低,因为
synchronized
关键字是对整个HashTable对象进行加锁,当一个线程访问HashTable的任何方法时,其他线程都必须等待,这导致在高并发场景下,锁竞争激烈,性能瓶颈明显。
- 优点:HashTable是线程安全的,其大部分方法(如put、get等)都被
- 使用Collections.synchronizedMap(new HashMap<>()):
- 优点:通过这种方式创建的线程安全的Map,底层仍然是HashMap,保留了HashMap的高性能特点。同时,利用
Collections.synchronizedMap
方法返回一个线程安全的Map包装类,对所有可能影响Map状态的方法进行了同步,保证了线程安全。 - 缺点:虽然解决了线程安全问题,但在高并发场景下,由于是对整个Map对象加锁,锁粒度较大,会导致性能下降。例如,一个线程执行put操作时,其他线程无法执行get操作,降低了并发性能。
- 优点:通过这种方式创建的线程安全的Map,底层仍然是HashMap,保留了HashMap的高性能特点。同时,利用
- 使用读写锁ReentrantReadWriteLock:
- 优点:可以实现读写分离,读操作可以并发执行,提高了读操作的并发性能。当多个线程同时进行读操作时,不会发生锁竞争,只有写操作需要获取独占锁。这种方式适用于读多写少的场景,能显著提高性能。
- 缺点:代码实现相对复杂,需要手动管理锁的获取和释放。并且如果读写操作比例不当,写操作频繁时,会导致读操作长时间等待写锁释放,影响系统整体性能。同时,如果使用不当,也可能导致死锁等问题。