HashMap及ConcurrentHashMap

一. Java 1.7

1. HashMap

        HashMap 默认初始容量是 16

        长度始终保持 2 的 n 次方

        扩容:每次扩容为原来的 2 倍

1)初始化:

        当初始化 HashMap 的大小时,HashMap 并不会使用我们传进来的 initialCapacity 参数直接作为初识容量。HashMap 构造函数会帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的 2 的幂。比如传 7 的话,hashmap 会帮我们初始化一个容量为 8 的 hashmap,传 9 的话,hashmap 会通过计算,帮我们创建一个容量为 16 的 hashmap。

2)加载因子为 0.75:

        实际存储容量  > initialCapacity *0.75时 会进行扩容,每次扩容都需要重建 hash 表,是非常影响性能的。同样设置过大浪费内存,因此设置一个合适的初始容量是有必要的。

可通过expectedSize /0.75F + 1.0F 计算默认初始的大小,expectedSize 为要存的数量

3)底层实现:

        底层使用一个 entry 对象数组 + 链表构成

        底层通过将 封装成一个对象 entry,然后通过计算 key 的 hashcode 值,再将hashcode 值 与 数组长度-1进行与运算后定位到数组 index,若该 index 无元素,则直接插入,若该 index 有元素,则遍历该位置链表,有该 key 则修改其值,没有则插入(Java 7 使用头插法,Java 8 使用尾插法)

2. HashTable

        HashTable 实现线程安全的原理:在可能产生并发安全问题的地方加上 synchronized 关键字,当某个线程操作hash表时给整个散列表加锁,保证并发线程安全性,但是显然这是不合理的,因为某个线程操作散列表的内容是有限的,但是锁了整个表,虽然保证了线程安全性,但是严重了影响性能

3. ConcurrentHashMap

底层原理:HashEntry +链表 + Segment + ReentrantLock 

        将整个 hashMap 散列表分段,每一段称之为一个 Segment ,默认大小是16,在并发情况下,通过 ReentrantLock 分段加锁保证并发线程安全,在 put  时,将封装成一个 HashEntry ,通过 key 的 hashcode 值先计算其位于哪一个Segment,若多个线程访问同一个 segment 时,只能有一个线程获得该 Segment 的锁,其余访问该 segment 的线程只能阻塞等待,但是访问不同 segment 的线程不受影响,可以并发执行

        Segment 数组大小称之为并发度,代表哈希表分的段数,一经初始化确定就不再变化

        相较于 hashTable 通过分段锁,在保证了 HashMap 的线程安全性的同时,大大提高了其性能

二. Java 1.8

1. HashMap

        底层使用 数组 + 链表 + 红黑树 构成

        底层通过将 封装成一个对象 Node,然后通过计算 key 的 hashcode 值,再将hashcode 值 与 数组长度-1 进行与运算后定位到数组 index,若该 index 无元素,则直接插入,若该 index 有元素,则遍历该位置链表,有该 key 则修改其值,没有则插入(Java 7 使用头插法,Java 8 使用尾插法)

        不同点:当链表元素个数大于等于 8 时,会判断当前数组长度是否大于64,小于64则进行扩容,大于64,则将该节点的链表转为红黑树

2.  ConcurrentHashMap

       JDK1.8 的 ConcurrentHashMap 数据结构使用的是和 HashMap 一样的数据结构:数组 + 链表 + 红黑树。ConcurrentHashMap 中包含一个 table 数组,其类型是一个 Node 数组;而 Node 是一个继承自 Map.Entry 的链表,而当这个链表结构中的数据大于等于 8 并且数据长度大于64,则将链表升级为 TreeBin 类型的红黑树结构


        并发安全性:整个Node数组使用 volatile 关键字修饰 ,保证了多线程下哈希表的可见性,CAS + synchronized 关键字方式时加锁的对象是每个链条的头结点,也就是锁定的是冲突的链表,所以再次提高了并发度

        当多线程访问同一个元素节点时:

        若该元素节点为空:则触发乐观锁机制,线程获得该 node 节点对象的版本号,在添加元素之前判断版本号是否与旧的保持一致,若一致则添加,若不一致则执行自旋操作

        若该元素节点不为空:则使用 synchronized 关键字进行加锁,锁住当前链表的头节点,只能有一个线程获得锁,其余线程必须阻塞等待,然后获得锁的线程进行操作

你可能感兴趣的:(Java,java,开发语言)