HashMap
JDK1.8对HashMap进行了优化。本章聊一聊优化内容。
- 数据结构:数组+链表——>数组+链表+红黑树
- put方法:头插法(多线程有一个循环链表的问题)——>尾插法
- 扩容机制
- 链表红黑树相互转换
HashMap数据结构:
JDK1.8之前:数组+链表。
JDK1.8及之后:数组+链表+红黑树。
为什么引入红黑树?
因为如果链表太长,则会导致遍历的效率低下。
引入红黑树可以提高遍历的效率。
HashMap的put方法(jdk1.7)
当向一个hashmap中put一个键值对时,
- 对key进行hash,得到所属table数组的下标。
- 接着判断数组当前下标位置是否为空?若为空,则把当前节点当头节点。
- 如果不为空,则开始遍历链表,依次equals比较。
- 如果有相同的key,则用当前的value替代之前的value。
- 如果未找到相同的key,则使用头插法插入。
HashMap的put方法(jdk1.8)
向一个hashMap中put一个键值对。
- 对key进行hash,得到所属table数组的下标。
- 接着判断数组当前下标位置是否为空?若为空,则把当前节点当成头节点。
- 如果不为空,则开始遍历链表,依次equals比较。
- 如果有相同的key,则用当前的value替代之前的value。
- 如果未找到相同的key,则使用尾插法插入。
HashMap的循环链表问题(jdk1.7)
- 因为jdk1.7的hashMap是头插法,所以在扩容转移元素的时候会使链表发生反转。
- 在多线程的情况下,两个线程同时触发扩容。
- 线程a,b都进入扩容状态,分别都会有一个e和e.next 用来遍历链表转移元素。
- 当a线程完成扩容,链表已经反转了,但是b还未遍历完链表,此时b线程的e.next指向的元素转移到了e所指元素之前,然后就形成了一个循环链表。
扩容机制
阈值=容量*负载因子。
当hashMap的元素个数大于阈值时,会触发扩容。
HashMap1.8的红黑树和链表转换
- 当数组长度大于64且链表长度大于8则会把链表转换成红黑树。
- 当红黑树元素的个数小于6则会把红黑树转换成链表。
ConcurrentHashMap
ConcurrentHashMap 是HashMap的线程安全版本。
jdk1.7和1.8的区别。
JDK7中ConcurrentHashMap是通过ReentrantLock+CAS+分段思想来保证的并发安全的,在JDK7的ConcurrentHashMap中,首先有一个Segment数组,存的是Segment对象,Segment相当于一个小HashMap,Segment内部有一个HashEntry的数组,也有扩容的阈值,同时Segment继承了ReentrantLock类,同时在Segment中还提供了put,get等方法,比如Segment的put方法在一开始就会去加锁,加到锁之后才会把key,value存到Segment中去,然后释放锁。
同时在ConcurrentHashMap的put方法中,会通过CAS的方式把一个Segment对象存到Segment数组的某个位置中。
同时因为一个Segment内部存在一个HashEntry数组,所以和HashMap对比来看,相当于分段了,每段里面是一个小的HashMap,每段公用一把锁,同时在ConcurrentHashMap的构造方法中是可以设置分段的数量的,叫做并发级别concurrencyLevel.
JDK8中ConcurrentHashMap是通过synchronized+cas来实现了。在JDK8中只有一个数组,就是Node数组,Node就是key,value,hashcode封装出来的对象,和HashMap中的Entry一样,在JDK8中通过对Node数组的某个index位置的元素进行同步,达到该index位置的并发安全。同时内部也利用了CAS对数组的某个位置进行并发安全的赋值。
JDK8的ConcurrentHashMap为什么使用Synchronized 来上锁?
JDK7中使用ReentrantLock来加锁,因为JDK7中使用了分段锁,所以对于一个ConcurrentHashMap对象而言,分了几段就得有几个ReentrantLock对象,表示得有对应的几把锁。
而JDK8中使用synchronized关键字来加锁就会更节省内存,并且jdk也已经对synchronized的底层工作机制进行了优化,效率更好。