Hashmap是否线程安全?为什么?

不安全,JDK7存在死循环和数据丢失问题。

数据丢失

  1. 并发赋值被覆盖:在createEntry方法中,新添加的元素直接放在头部,使元素之后可以被更快访问,但如果两个线程同时执行到此处,会导致其中一个线程的赋值被覆盖
  2. 已遍历区间新增元素丢失:当某个线程在transfer方法迁移时,其他线程新增的元素可能以及落在已经遍历过的哈希槽上。遍历完成后,table数组引用指向了newTable,新增元素丢失
  3. 新表被覆盖:如果resize完成,执行了table = newTable,则后续元素就可以在新表上进行插入。但如果多线程同时resize,每个线程都会new一个数组,这是线程内的局部对象,线程之间不可见。迁移后resize的线程会赋值给table线程共享变量,可能会覆盖其他线程操作,在新表中插入的对象都会被丢弃。

死循环

扩容时resize调用transfer使用头插法迁移元素,虽然newTable是局部变量,但原先的table中Entry链表是共享的,问题根源是Entry的next指针并发修改,某线程还没有将table设为newTable时用完了CPU时间片,导致数据丢失或死循环。

JDK8在resize方法中完成了扩容,并改为尾插法,不会产生死循环,但并发下仍可能丢失数据。可用ConcurrentHashMap 或 Collections.synchronizedMap包装成同步集合。

JDK7与JDK8的hashmap有什么区别:

JDK7是数组 + 链表,JDK8是数组 + 链表/红黑树

  1. 链表插入方式不同:1.7之前,链表元素插入采用头插法,每当有新节点进入时,会插入在链表头部,由于不用遍历链表,这种插入方式效率高;1.8以后当节点插入时,因为需要判断元素个数而遍历链表(是否达到转为树的阈值),所以顺带改为尾插,即插到链表尾部,这解决了多线程下可能引发的死锁问题,因为头插法的链表在扩容移动时,会被逆序,即后插入的先处理,如果这时候有另一线程进行get操作,就可能引发死锁
  2. 插入时机不同:1.7之前是扩容后再插入新的数据,并且不会先计算值的哈希值,最后单独计算;1.8之后是先插入再扩容,插入值和大家一起计算新的哈希值