前言
HashTable是线程安全的,它在所有涉及到多线程的操作都加上了synchronized关键字来锁住整个table,这就意味着所有线程都需要竞争一把锁,在多线程情况下,是安全的,但效率也是非常低的。
在多线程环境下,其实不需要对全部数据进行加锁,使用锁分离技术,可以降低锁的粒度,利用多个锁来控制多个小的table。
concurrentHashMap
JDK1.7的实现
ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成的。
Segment数组的将一个大的table分割成多个小的table来进行加锁,也就是锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
初始化
- ConcurrentHashMap的初始化是会通过位与运算来初始化Segment的大小。Segment的大小取值都是以2的N次方,无关concurrencyLevel的取值,当然concurrencyLevel最大只能用16位的二进制来表示,即65536,换句话说,Segment的大小最多65536个,没有指定concurrencyLevel元素初始化,Segment的大小ssize默认为16。
- 每一个Segment元素下的HashEntry的初始化也是按照位于运算来计算,用cap来表示。HashEntry大小的计算也是2的N次方(cap <<=1), cap的初始值为1,所以HashEntry最小的容量为2。
put操作
- 对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置。
- Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。
get操作
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
size操作
在计算ConcurrentHashMap的元素大小时,由于是在并发环境下操作的,在计算size的时候,可能存在并发的插入数据,就会导致你计算出来的size和你实际的size有差别(在return size的时候,插入了多个数据),要解决这个问题,JDK1.7版本用两种方案。
- 第一种方案:使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的。
- 第二种方案:如果第一种方案不符合,就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回。
JDK1.8
待