学习java的人都知道,HashMap是线程不安全的,不能在多线程环境下共享一个HashMap变量,在jdk1.7中的HashMap的实现,多线程下共享HashMap会导致死循环(有hash冲突的时候,链表上可能存在环导致死循环),但是在jdk8中对这个写法作了优化,不会导致死循环了,但是依然是线程不安全的,多线程下数据是不准确的。
这里不是想说HashMap为什么不安全,然后分析他的源码,然后ConcurrentHashMap是线程安全,继续分析它的源码,这里想要大概解释下,jdk是如何将HashMap变成线程安全的。
最简单直接的方法,对HashMap的所有操作都加上同一把互斥锁,即锁住整个HashMap,将所有的操作都变成串行的。其实这就是jdk的HashTable。它肯定是线程安全的,但是因为所有的操作都是串行的,并发度为1,所有的操作都是串行的,那么同一时间就只能允许一个线程执行,效率是很低的。所以对这种方式的优化就是提高并发度,就有了jdk的ConcurrentHashMap。
HashMap的基本结构并不难:table数组就是hash桶,然后每个hash桶使用链表来解决hash冲突的问题。
提高并发度最直观的就是别一次性锁住整个table,少锁一点,这样就允许多个线程同时操作了。所以最好的情况就是锁的粒度就是table元素,即锁只是锁住一个桶位,因为一个线程操作操作首先会先定位到桶位上,然后实际操作的实际是链表。但是有一个问题,HashMap中可是存储了整个桶的全局数据变量size的,这个变量绝对不能多线程修改,否则size就不准了,所以没办法做到只是锁住一个hash桶位。接下来就看jdk大神的表演。
jdk7中,引入Segemnt,将原来整个table进行拆分成不同小的segment,可以认为每个segment是一个原来的Map,table的是一个Segment为元素的数组。在对Map进行操作的时候需要两次hash,一次hash定位segment,然后对segment进行上锁,然后修改segment内的内容,而size也是Segment的属性。这样就增大了整个Map的并发度。这就是jdk7的ConcurrentHashMap
在jcu包中,有一些CAS的类型封装,如AtomicLong等,这些都是都是在死循环中CAS,所以说如果竞争特别大,就有可能一直在CAS,导致cpu飙升。所以就有了LongAddr,它尽可能去减少竞争,是因为内部有一个cell,如果是多线成修改,那么这个线程实际修改的是对应cell的值,而在get的时候,将所有的cell数据进行相加,就得到了一个最终的值。也正是因为这样,LongAddr是一个最终一致的结构,当一致性要求特别高的时候,没办法使用这个接口。
而jdk8中的ConcurrentHashMap也是利用了LongAddr同样的方式来管理size变量。
在jdk8中,ConcurrentHashMap中已经没有Segment了,结构又回到了和HashMap一样。而且在修改Map的时候,首先是CAS修改数据,如果修改不成功,再对对应的桶位使用synchronized加锁(不要谈synchronized而色变,优化后的synchronized效率不必lock差,差的是灵活性),而对于size的修改就是利用LongAddr的思想,所以在java8的ConcurrenthashMap的实现中,可以发现LongAdder的实现相似的身影:
private transient volatile int cellsBusy;
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
在size()的时候,会将这个值进行累加,和LongAddr思想是一模一样的,实际中还有个baseCount,会将cell中的值利用CAS合并到baseCount中的,具体在什么时候合并,都是写细节的问题。包括cell也是有冲突的时候才会用到,没有冲突的时候CAS就累加baseCount成功了。
所以,ConcurrentHashMap也是一个弱一致的结构,强一致的场景慎用。