一.HashMap为什么是线程不安全的?
1.非同步操作:HashMap的操作不是线程同步的,多线程时会出现可见性问题。
2.非原子操作:HashMap的操作是非原子性的,多线程时会导致原子性问题。
(1)put()操作
a.jdk1.7的HashMap的put()操作,采用了链表头插法。多线程情况下,在扩容时可能会出现链表死循环的问题。
b.jdk1.8的HashMap的put()操作,虽然改用链表尾插法避免了死循环的问题,但仍会因为非原子操作而产生问题:
问题1:发生哈希冲突本该以链表形式存储冲突的数据,但在多线程情况下,可能会由于线程上下文切换而导致忽视了哈希冲突,出现值覆盖的情况。
问题2:put()操作在添加完元素后,还会对size变量进行修改,由于该操作不是原子性的,同样会引发多线程对size的修改产生冲突的情况。
(2)resize()操作:resize()操作涉及到了对节点的复制和重定位,若非原子操作,则在扩容期间可能会有其他线程对数组进行并发修改,产生问题。
3.HashTable是线程安全的,但其线程安全的实现是通过在方法上加synchronized锁,性能低下。
二.说一下jdk1.7和jdk1.8的ConcurrentHashMap的区别
1.数据结构不同
(1)jdk1.7的ConcurrentHashMap使用segment数组和HashEntry数组实现
a.segment[]:大数组,将HashMap的table数组切分成若干段小数组HashEntry,segment数组的每个元素就对应一段HashEntry。
b.HashEntry[]:小数组,采用数组+链表的形式存储数据节点。
(2)jdk1.8的ConcurrentHashMap使用与HashMap一样的结构:数组+链表/红黑树
(3)jdk1.7的ConcurrentHashMap访问数据需要进行两次定位,第一次定segment元素位置,第二次定HashEntry元素未知;而jdk1.8只需要进行一次定位。
2.加锁方式不同
(1)jdk1.7的ConcurrentHashMap的加锁对象是segment数组的一个元素,即一次锁一整段HashEntry,用的是可重入锁ReentrantLock。
(2)jdk1.8的ConcurrentHashMap的加锁对象是当前槽位的头节点,一次只锁一个槽位,用的是synchronized锁。
(3)jdk1.8用synchronized锁一个槽位,相比于jdk1.7用ReentrantLock锁一整段HashEntry,分段粒度更细,性能更好。
三.jdk1.8的ConcurrentHashMap是如何保证线程安全的?
1.table数组用volatile关键字修饰,保证了可见性和有序性。
2.put()操作
在最外层加一个乐观锁,或者说是一个for死循环,通过这个循环来完成多层条件逻辑的执行:
(1)判断table数组是否为null或长度为0,若满足则初始化数组,并进入下一次循环
(2)通过索引公式定位到目标槽位,如果目标槽位为空,则通过CAS操作插入数据
a.若CAS操作成功,则退出循环
b.若CAS操作失败,说明有其他线程修改,进入下一次循环
(3)判断当前数组是否在扩容,若满足则帮助数组扩容。无论是否在扩容,都会进入下一次循环
(4)无论是CAS操作失败还是目标槽位已有数据,都会走到当前判断逻辑,即说明当前槽位已经被占有。因此接下来执行判断,若满足以下条件:不需要覆盖原值、槽位节点的hash与新节点的hash相同、槽位节点的key与新节点的key相同,则直接将当前槽位节点的值返回;否则进入下一次循环
(5)为当前槽位的头节点申请synchronized锁,申请到锁后,执行HashMap的添加逻辑。
退出乐观锁后会进行判断:是否要扩容或链表转红黑树,若满足则执行对应逻辑。
上述五个条件,乐观锁的每次循环完成一个条件的处理,每多执行一次循环,则往下推进一个条件判断。
put()操作基于CAS操作和synchronized锁保证线程安全,且只有在发生哈希冲突时才需要加synchronized锁,降低了锁的粒度,提高了效率。
3.get()操作:由于table数组已经被volatile关键字修饰,因此get()操作无需加锁。
四.对于ConcurrentHashMap,在执行put()操作时,为什么key和value不能为null?
为了防止二义性。
1.对于HashMap:用于单线程环境
(1)在put()时允许key和value为null。key为null则直接映射到0号槽位,value为null则直接存一个null值。
(2)如果get()方法得到一个null值,为了判断是因为不存在这个key而返回一个null还是本来就存了一个null,可以调用containsKey()判断是否存在key,从而解决二义性问题。
2.对于ConcurrentHashMap:用于多线程环境
(1)在put()时如果key或value为null,则直接抛出NullPointException。
(2)如果get()方法得到一个null值,是无法通过containsKey()来解决二义性问题的,因为在此期间可能会有其他线程对该槽位进行并发修改,从而导致判断结果与实际结果出现差异。