这两天找实习问的两次的题目回来特意梳理下。
一:HashMap:key可以存null值,线程不安全的,通过源码可以看到是继承了AbstractMap,HashMap的默认初始大小是16,扩容因子是0.75,每次扩容*2,在1.8之前是采取数组+链表实现的,1.8采用数组+链表+红黑树(发现红黑树不是很熟悉,下一篇写写红黑树。。)。
1 HashMap是利用哈希法实现的,有一个Entry数组和若干链表组成,当我们使用了put(key,value)方法时,首先会调用hashCode()方法获取到hashCode值对数组长度取余找到在数组上的存储位置,然后判断位置是否有元素,没有的话存入Entry,如果这个位置不为空则调用equals()方法判断key值是否重复,如果重复则直接覆盖,不相等的话会存储到链表中,新存入值指向旧的值,jdk1.8之后但链表长度大于8的时候链表就变成了红黑树为了减少查找的时间。链表长度为8,链表转树,链表长度为6,树转链表,采用这个临界值我觉得简直神来之笔,如果不停插入删除,链表的长度8的上下不停浮动不停转换树和链表效率会很低。get()方法时也是类似的逆向方法。
2 HashMap为什么线程不安全,HashMap的大小超过了负载因子定义的容量时会创建原来HashMap两倍大小的数组,重新调整rehash:rehash过程中会重新计算分配存储的桶:1对索引数组元素遍历。2对链表上的每一个节点遍历,转移元素到新的Hash表的头部,3循环2 之链表节点全部转移,4循环1索引数组全部转移。 转移的时候是逆序讹,前1>2>3 后3>2>1,高并发情况下会造成循环链表造成死锁。
二:Hashtable:key&value不可以存null值,会报NullPointerException,继承Dictionary,是线程安全的,通过查看源码可以看到很多方法加了synachronized关键字(synachronized关键字也不是很熟,下下篇写这个。。。)Hashtable初始容量11,加载因子0.75.扩容方式是old*2+1。扩大方式和HashMap出不多。Hashtable采用synachronized进行同步,相当于所有的线程进行读写时都去竞争一把锁,效率十分低。ConcurrentHashMap 代替了Collection.synachronized (new HashMap()),
三:ConcurrentHashMap:ConcurrentHashMap底层采用锁机机制,执行put方法的线程会获得锁,只有当此线程的put方法执行结束后才会释放锁,所以保证了put方法每次只有一个线程进去执行,保证了线程安全。分段锁是ConcurrentHashMap的核心,当进行插入操作时,先通过key的hash计算桶的位置,在锁住这个桶在执行插入操作。这种方式允许并发的对不同的桶进行插入操作,但插入的是同一个桶的话还是会由于等待锁的获取而进入阻塞。并发插入、并发扩容以及无阻塞读写。 插入操作它通过使用 CAS 操作(又一个新知识m)和分段锁机制来实现并发插入。当插入元素个数超过负载因子,则会进行扩容,并且通过实现并发扩容来减少扩容的占用的时间,它将 hash 表分为多段,参与扩容的线程先申请一段,完成之后再申请另一端,通过多个线程写作来完成扩容。读操作是无阻塞的,不需要进行加锁,它与 HashMap 的实现原理类似,它仅仅利用 volatile 变量(Mark)来保证内存可见性,它属于弱一致类型,即在一个时刻同时有线程调用 get(key) 和 put(key , value),读线程可能能获取到 value,也可能获取不了。
put 的工作流程:
1. 检查 key 和 value 是否为空,若是,则抛出 NullPointerException,结束;否则执行 2;
2. 计算 key 的 hash 值,计算方式与 HashMap 的实现一样;
3. 通过 hash 计算 index,判断当前 index 位置是否存在元素,若无元素,则采用无锁的 CAS 操作(mark)尝试插入新节点,若成功,则执行;失败,则重新执行 3;若当前位置有元素,则执行 4;
4. 判断当前是否正在扩容,若正在扩容,则加入协助扩容,扩容完毕后则回去重新执行 3;若不是正在扩容,则执行 5;
5. 锁住当前 index 位置的节点,遍历该位置的所有节点,判断是否存在相同的元素,存在则替换旧值;不存在则插入一个新的节点保存元素,然后执行 6;
6. 给容器的当前元素个数 count 加 1,若超出负载因子,则进行扩容操作 。
扩容:
1. 根据 CPU 个数和当前表的长度,计算一轮重哈希需要对多少长度的表进行重哈希;
2. 如果是第一个触发扩容的线程,创建新表,长度为原来的两倍;
3. 循环对旧表的元素进行重哈希,直到全部位置都重哈希完毕
3. 获取新一段需重哈希的初始 index,每一段的长度为 stride,获取后该线程就负责对 [index - stride , index] 的元素进行重哈希,由于存在并发扩容,因此获取并更新 transferIndex 需要循环使用 CAS 操作来获取
4. 对段中的每一个位置都进行重哈希,哈希过程中需要对头元素进行加锁,避免由于扩容和 put、delete 操作冲突,具体重哈希原理与 HashMap 类似。
每个线程会各自负责一段的重哈希,当该段完成后再去负责下一段。
get 操作
ConcurrentHashMap 的读操作是无阻塞的,不需要进行加锁,它与 HashMap 的实现原理类似,它仅仅利用 volatile 变量来保证内存可见性,它属于弱一致类型,即在一个时刻同时有线程调用 get(key) 和 put(key , value),读线程可能能获取到 value,也可能获取不了,这与 JMM 有关,可以通过 happen-before 关系来分析线程行为。