HashMap
JDK7:
在JDK7中,HashMap通过数组加链表的形式存储,当元素个数达到阈值,并且数组下标已经存在元素,则会进行扩容,如果数组下标不存在元素,则直接添加,不会扩容。
JDK7中添加元素使用的是头插法,在高并发的环境下可能会导致死链。
新增对象丢失原因:
- 并发赋值时被覆盖。
- 扩容中的数据迁移,新增的数据落在了原来的HashMap中,并且所在的哈希槽已经被遍历过。
- 多个线程同时执行resize方法,每个线程都会创建Entry,最后的赋值中会覆盖其他线程的数据。
- 迁移丢失。在并发迁移过程中,next被提前设置为null。
JDK8:
在JDK8中,HashMap通过数组加链表或红黑树的节后进行存储。当链表的长度大于等于8(8来自于泊松分布),并且哈希槽的个数不小于64的时候才会进行链表转红黑树,如果链表大于等于8,但是哈希槽小于64,则会执行resize方法进行扩容。当红黑树节点小于等于6的时候,红黑树会退化成链表。
默认容量为16,默认负载因子是0.75,所以当节点个数大于
16 * 0.75 = 12
时就需要扩容。JDK8中添加数据采用的尾插法,避免了死链还保证了数据有序性,同时统计了链表的长度,方便树化和链表化。
ConcurrentHashMap
JDK7:
在JDK7中ConcurrentHashMap是由Segment[]数组加上HashEntry[]组成的。底层用的数组加链表。
原理上ConcurrentHashMap采用的是分段锁,其中的Segment继承于ReentrantLock,它的锁粒度比HashTable更细,当一个线程来访问时,只会占用对应的Segment,对其他Segment不想影响。
在进行put时,通过
scanAndLockForPut
方法获取到锁,首先通过自旋尝试获取锁,当自旋次数达到MAX_SCAN_RETRIES
时,会强行上锁获取对象。get逻辑比较简单,因为Entry的value属性都是通过volatile修饰的,可以保证可见性,所以不需要加锁。
JDK8:
JDK8抛弃了原有的分段锁,使用的是
CAS + synchronized
。在putVal时,当该table[i]为空时,直接通过cas进行put;当table[i]不为空,并且table[i]的hash值是-1时,表示其他线程正在扩容,调用
helpTransfer()
帮助一起扩容;当table[i]有值,且没有在扩容时,直接通过synchronized锁住哈希槽进行添加。