jdk1.8的HashMap采用的是数组+链表或红黑树
这么做的目的,上篇文章已经解释了:
① 解决链表逆序和死循环的问题
② 优化发生hash碰撞情况下,提升get速度,时间复杂度由O(n)提升到O(logn)。
这里HashMap的链表和红黑树是随着链表的长度或树的深度动态转换的:
当链表的长度超过8时,自动转为红黑树。
当红黑树的深度小于6时,自动转为链表。
为什么转为红黑树是8,而转为链表为6?为什么两者不能都设置为8呢?
首先,转为红黑树,设置成8,是同过一定的概率总结归纳出来,当链表的长度超过8,才会影响HashMap的get速度,或者如果设置的小转换提升效率不高;如果设置过大,那么会直接影响效率。
其次,如果设置成一样,会在8附近频繁切换,影响效率。
HashMap是否线程安全?
》不是线程安全的
jdk1.7多线程操作HashMap会造成死循环、数据丢失和数据覆盖;jdk1.8会造成数据覆盖。
举个例子:两个线程A和B,A和B都想插入数据,且两个插入的位置为同一个桶位Index,两个线程读取时发现这个位置都是空的,可以直接进行插入操作。当其中一个线程A操作完,将数据插入index这个位置;线程B再去插入时就会将数据覆盖掉。
平时如何解决线程不安全的?
使用HashTable和Collections.synchronizedMap以及ConcurrentHashMap来实现线程安全的Map。
1、HashTable直接在方法上加关键字synchronized,将整个数组锁起来,锁粒度比较大。
2、Collections.synchronizedMap 使用Collections集合工具的内部类,通过传入 Map封装出一个synchronizedMap对象,内部定义了一个对象锁,方法内通过对象锁实现。
3、ConcurrentHashMap通过分段锁实现,降低锁粒度,大大提高并发度。(jdk1.8后放弃了分段锁)
jdk8里ConcurrentHashMap为什么放弃了分段锁?
首先,我们先了解下ConcurrentHashMap中的分段锁实现原理:
ConcurrentHashMap成员变量使用volatile修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前索引的节点。
ConcurrentHashMap的分段一开始就已经确定,后期是不能扩容的。每段称为segment,这个segment是可以扩容的,它的内部结构1.7是数组+链表的形式,1.8为数组+链表/红黑树。
Segment继承了可重入锁ReentrantLock,有了锁的功能,每个锁控制一段,当segment越来越大时,锁的粒度也会变得越来越大。
其次,来了解它的优缺点:
分段锁的优点:
可保证不同段的map同时操作,并发执行,操作同段时竞争和等待。相对于对整个map进行synchronized同步操作是有优势的。
分段锁缺点:
①分成很多段比较浪费空间(不连续、碎片化)。
②同时竞争同一段锁的概率比较小,反而会造成更新等操作长时间等待。
③ 某个段很大时,会造成分段锁性能下降。
jdk8里ConcurrentHashMap为什么使用了synchronized而不是用ReentrantLock?
①减少内存开销。可重入锁ReentrantLock使用,需要继承AQS来实现同步操作,会增加额外的内存开销。而jdk1.8中只需要对头节点进行同步。
②内部优化。可重入锁ReentrantLock是Api级别的,优化空间小。而synchronized是JVM直接支持的,JVM能在运行时做出相应的优化:锁粗化,锁消除,锁自旋等等。