1.7版本 采用的还是分段锁,底层采用数组+链表的存储结构。Segment继承ReentrantLock用来充当锁
一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组,下面我们通过一个图来演示一下 ConcurrentHashMap 的结构:
Get方法:
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.再次通过hash值,定位到Segment当中数组的具体位置。
Put方法:
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.获取可重入锁
4.再次通过hash值,定位到Segment当中数组的具体位置。
5.插入或覆盖HashEntry对象。
6.释放锁。
size方法
既然每一个Segment都各自加锁,那么我们在统计size()的时候怎么保证解决一直性的问题,比如我们在计算size时,有其他线程在添加或删除元素。
ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:
1.遍历所有的Segment。
2.把Segment的元素数量累加起来。
3.把Segment的修改次数累加起来。
4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
7.释放锁,统计结束。
可以看出JDK1.7的size计算方式有点像乐观锁和悲观锁的方式,为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性。
JDK1.8版本
ConcurrentHashMap采用Node + CAS + Synchronized,底层采用数组+链表+红黑数的存储结构。
和 hashMap 1.8 一样9转红黑树 6转链表
CAS的全称叫“Compare And Swap”,也就是比较与交换,他的主要操作思想是:
首先它具有三个操作数,a、内存位置V,预期值A和新值B。如果在执行过程中,发现内存中的值V与预期值A相匹配,那么他会将V更新为新值B。如果预期值A和内存中的值V不相匹配,那么处理器就不会执行任何操作。CAS“无锁定算法”,因为线程不必再等待锁定,只要执行CAS操作就可以,会在预期中完成。
CAS+volatile 实现了 线程安全
CAS ABA问题概述:
维基百科上面的形象描述:
你拿着一个装满钱的手提箱在飞机场,此时过来了一个火辣性感的美女,然后她很暖昧地挑逗着你,并趁你不注意的时候,把用一个一模一样的手提箱和你那装满钱的箱子调了个包,然后就离开了,你看到你的手提箱还在那,于是就提着手提箱去赶飞机去了。
AtomicStampedReference和 AtomicMarkableReference是通过版本号(时间戳)来解决ABA问题的,我们也可以使用版本号(verison)来解决ABA。
即乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1
操作,否则就执行失败。