引入:如何优化Hashtable?
通过锁细粒度化,将整个锁拆解成多个锁进行优化
这儿就引入了早期的ConcurrentHashMap
使用的是分段锁技术,把Bucket分成几段来存储,为每一段数据都配一把锁(segment)【这样做的原因是:为每个Bucket都添加一把锁的话,资源消耗大,比如1w个Bucket就要有1w个锁】
当某个程序访问某个数据段的时候,就会获得当前数据段(segment)的锁,操作其子数组。而如果还有程序要访问,就只能被阻塞,而其他未被访问到的数据段依旧可以访问
早期ConcurrentHashMap分配了16个segment,理论上较Hashtable会提高16倍的效率
注:ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。
Segment是一个可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数据。
一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素。当对HashEntry数组的数据进行修改时,必须首先获得与它对应的segment锁。
这儿就引入了现如今的ConcurrentHashMap,通过CAS和synchronized使锁进一步细化,结构和JDK1.8的HashMap相同
(附:HashMap是在package java.util包下的,但是ConcurrentHashMap是在package java.util.concurrent下的)
concurrent分段如何实现的?具体一点
源码分析:
里面有很多参数和HashMap相同
也有独特的参数,比如这个控制table大小的变量
进入到源码中,直接看put方法,发现里面一开始就有个对key、value判空的条件:
也就是说ConcurrentHashMap是不能够存放空值的,而HashMap则可以。
然后下一行,能够看到ConcurrentHashMap对hash值的确定:
由于对数组更新是使用CAS来进行更新的,所以需要不断去做失败重试,直到成功为止
如果发生了hash碰撞,ConcurrentHashMap的处理逻辑:
ConcurrentHashMap:put方法的逻辑
1.判断Node[]数组是否初始化,没有就进行初始化操作
2.通过hash定位数组的索引坐标,是否有Node节点,如果没有就使用CAS进行添加(链表的头结点),添加失败则进入下一次循环
3.如果检查到内部正在扩容,就帮助它一块扩容
4.如果f!=null,则使用synchronized锁住f元素(f元素就是链表或者红黑二叉树的头元素)
4.1 如果是Node,则执行链表的添加方法
4.2 如果是TreeNode,则执行树添加结构
5.判断链表长度是否已经达到临界值8(8是默认值,可以调整),当节点数超过这个值,就需要把链表转化为树结构
ConcurrentHashMap的总结:比起Segment,锁拆的更细
1.首先使用无锁操作CAS插入头结点,失败则循环重试
2.若头结点已经存在,则尝试获取头结点的同步锁,再进行操作