HashMap的初始化与扩容
- HashMap 默认容量是 16,默认的负载因子是 0.75f
- 构造方法只会初始化一个空的 Node 数组,真正的初始化过程是在 put 方法中的 putVal 里面调用的,主要核心方法是 resize 方法
- HashMap 的容量固定为 2的n 次方,也就是如果我们传入了初始化容量,HashMap 不会使用我们传入的容量,而是会帮我们计算与我们传进去的参数最接近(大于等于)的一个 2n 的值作为容量
- HashMap 内部会存在一个阈值(threshold),该值为容量和负载因子的乘积,当 HashMap 里面的容量大于等于阈值时,会触发扩容。
- 负载因子越大则散列表的装填程度越高,也就是能容纳更多的元素,元素多了,链表大了,所以此时索引效率就会降低(时间换空间)。反之,负载因子越小则链表中的数据量就越稀疏,此时会对空间造成烂费,但是此时索引效率高(空间换时间)。
- 因为扩容是一个拷贝数组的过程,比较耗费资源。建议在初始化的时候指定容量,才能最大限度的利用 HashMap 的性能。
ConCurrentHashMap
concurrentHashMap默认会初始化一个长度为16的数组,它的核心仍然是哈希表,当hash冲突比较多的时候,会造成链表长度过长的问题,这种情况下会使得concurrentHashMap中的一个数组元素的查询复杂度增加,所以在jdk1.8中引入了红黑树这样的数据结构,当数组长度大于64,并且链表的长度大于等于8的时候,单向链表就会转换成红黑树,另外,随着concurrentHashMap的一个动态扩容,一旦链表的长度小于8,红黑树会退化成单向链表
ConcurrentHashmap线程安全在jdk1.7版本是基于分段锁实现,在jdk1.8是基于CAS+synchronized实现。
concurrentHashMap的基本功能:concurrentHashMap本质上是一个hashmap,功能和hashmap是差不多的,但是concurrentHashMap在Hashmap的基础上提供了并发安全的一个实现,并发安全的主要实现是通过对于链表或红黑树的头节点去加锁,来保证数据更新的安全性。
concurrentHashMap在性能方面做优化,比如在扩容的时候引入了多线程并发扩容的实现,简单来说就是多个线程对原始数组进行分片,分片之后,每个线程去负责一个分片的数据迁移,从而提升了扩容过程中数据迁移的一个整体效率。
ConCurrentHashMap是如何保证线程安全的?
jdk1.7底层实现:在jdk1.7中它使用的是数组+链表的形式实现,而数组又分为大数组Segment和小数组HashEntry,
通过put方法来看jdk1.7中ConcurrentHashMap是如何保证线程安全的,Segment本身是基于ReentrantLock实现的加锁和释放锁的操作,这样就能保证多个线程,同时,访问ConcurrentHashMap时,同一时间只有一个线程能操作相应的节点,这样就保证了ConcurrentHashMap的线程安全了,也就是说ConcurrentHashMap的线程安全是建立在Segment加锁的基础上的,所以我们把它称之为分段锁。
如下图所示,jdk1.8实现,
在jdk1.7中,ConcurrentHashMap虽然是线程安全的,但因为它的底层实现是数组+链表形式,所以在数据比较多的情况下访问是很慢的,因为要遍历整个链表,而jdk1.8则使用数组+链表/红黑树的方式优化了ConcurrentHashMap的实现,具体实现结构如上图。
链表升级为红黑树的规则
当链表长度大于8,并且数组长度大于64时,链表就会升级为红黑树结构,ConcurrentHashMap在jdk1.8虽然保留了Segment的定义,但这仅仅是为了保证序列化时的兼容性,不再有任何结构上的用处了。
JDK1.8ConcurrentHashMap线程安全的实现
在JDK1.8中ConcurrentHashMap使用的是CAS+volatile或synchronized的方式来保证线程安全的,它的核心实现源码如上图,从上述源码可以看出,在jdk1.8中,添加元素时首先会判断容器是否为空,如果为空则使用volatile加CAS来初始化,如果容器不为空则根据存储的元素计算该位置是否为空,如果为空则利用CAS设置该节点;如果不为空则使用synchronize加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转换为红黑树,这样就能保证并发访问时的线程安全了。
我们把上述流程简化一下,我们可以简单地认为在jdk1.8中,ConcurrentHashMap是在头节点加锁来保证线程安全的,锁的粒度相比Segment来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提升了,而且jdk1.8使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的O(n)优化到了O(logn)的时间复杂度。
总结:
ConcurrentHashMap在jdk1.7时使用的是数组+链表的形式实现的,其中数组分两类,大数组Segment和小数组HashEntry,而加锁是通过给Segment添加RentrantLock锁来实现线程安全的,而JDK1.8中ConcurrentHashMap使用的是数组+链表/红黑树的方式实现的,它是通过CAS或synchronized来实现线程安全的并且它的锁粒度更小,查询性能也更高。