concurrentHashMap用 transient volatile Node<K,V>[] table修饰,使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的;
ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段node,也就是将这个大的数组分成了几个小的片段node,而且每个小的片段node上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段node,然后再在这个片段上面进行插入,而且这里还需要获取node锁;ConcurrentHashMap让锁的粒度更精细一些,并发性能更好;
在jdk1.8里面ConcurrentHashMap锁的粒度,是数组中的某一个节点,而在jdk1.7里面。它锁定的是Segment,锁的范围要更大,所以性能上它会更低。
JDK1.7中ConcurrentHashMap采用的就是分段锁,就是把整个table分割为n个部分,每个部分就是一个Segment;每个Segment中由HashEntry数组组成,这里的HashEnrty数组结构和HashMap中的相同,由数组+链表组成; 当对某个Segment加锁时,其他的Segment并不会受影响,理想状态下,所有线程操作的都是不同的segment,就可以降低锁的竞争,而且还是线程安全的;
Segment继承于ReentrantLock,tryLock和lock是ReentrantLock中的方法,tryLock不会阻塞,抢锁成功就返回true,失败就返回false,lock方法抢锁成功则返回,失败则会进入同步队列,阻塞等待获取锁;Segment类似于HashMap的结构,内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock);
通过key的hash定位到具体的segment,再通过一次hash定位到具体元素,由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值;
JDK1.8中的ConcurrentHashMap实现,完全重构了JDK1.7,不再使用分段锁,而是给数组中的每个头节点都加锁,并且用的是synchronized。整体采用CAS+synchronized来保证并发的安全性;JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点);
JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了;
有四种状态:无锁,偏向锁,轻量级锁和重量级锁。
get方法不用加锁,是非阻塞的,重写Node类,通过volatile修饰next来实现每次获取都是最新值;
当通过get(k)获取对应的value时,如果获取到的是null时,无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射;HashMap是非并发的,可以通过contains(key)来做这个判断;而支持并发的Map在调用m.contains(key)和m.get(key)的时候,m可能已经不同了;
1、如果新增节点之后,所在链表的元素个数达到了阈值 8,则会调用treeifyBin方法把链表转换成红黑树,不过在结构转换之前,会对数组长度进行判断,实现如下:如果数组长度n小于阈值MIN_TREEIFY_CAPACITY,默认是64,则会调用tryPresize方法把数组长度扩大到原来的两倍,并触发transfer方法,重新调整节点的位置;
2、调用put方法新增节点时,在结尾会调用addCount方法记录元素个数,并检查是否需要进行扩容,当数组元素个数达到阈值时,会触发transfer方法,重新调整节点的位置;
扩容状态下其他线程对集合进行插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode 节点会触发扩容 ;
putAll 批量插入或插入节点后发现存在链表长度达到 8 个或以上,但数组长度为 64 以下时会触发扩容 ;
注意:桶链表长度达到 8 个或以上,并且数组长度为 64 以下时只会触发扩容而不会将链表转为红黑树 ;
当数组长度不够的时候,ConcurrentHashMap它需要对数组进行扩容,而在扩容时间上,ConcurrentHashMap引入了多线程并发扩容的一个实现,简单来说多个线程对原始数组进行分片,分片之后,每个线程去负责一个分片的数据迁移,从而去整体的提升了扩容过程中的数据迁移的一个效率;
只要插入的位置扩容线程还未迁移到,就可以插入,当迁移到该插入的位置时,就会阻塞等待插入操作完成再继续迁移 ;
get操作全程不需要加锁是因为Node的成员val是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的;