1.7 中采用Segment + HashEntry ,用的是的ReentrantLock
1.8 Node + CAS + Synchronized,Synchronized锁的是Node节点
JDK7的ConcurrentHashMap:
ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
Segment 数组默认是16,不可扩容;Segment 内部是由 数组+链表 组成的。
put操作:根据 hash 值很快就能找到相应的 Segment,之后找到Segment内部数组的位置(此部分需要加锁,如果不够就进行扩容)。
ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽来说,在插入第一个值的时候进行初始化。
get操作:
1、计算 hash 值,找到 segment 数组中的具体位置,或我们前面用的“槽”
2、槽中也是一个数组,根据 hash 找到数组中具体的位置
3、到这里是链表了,顺着链表进行查找即可
size操作
- 第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的
- 第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回
Unsafe 类
// 通过内存屏障保证有序性,再通过volatile保证将对指定地址的操作马上写入到共享到主存中,这样配合get时候每次拿到的值都是最新的
void sun::misc::Unsafe::putObjectVolatile (jobject obj, jlong offset, jobject value)
{
write_barrier ();// 写屏障
volatile jobject *addr = (jobject *) ((char *) obj + offset);
*addr = value;
}
void sun::misc::Unsafe::putObject (jobject obj, jlong offset, jobject value)
{
jobject *addr = (jobject *) ((char *) obj + offset);
*addr = value;
}//用于和putObjectVolatile进行对比
// 返回前加入read,这个读屏障作用就是强制去读取主内存中读数据,确保读到的一定是最新的
jobject sun::misc::Unsafe::getObjectVolatile (jobject obj, jlong offset)
{
volatile jobject *addr = (jobject *) ((char *) obj + offset);
jobject result = *addr;
read_barrier ();// 读屏障
return result;
}
void sun::misc::Unsafe::putOrderedObject (jobject obj, jlong offset, jobject value)
{
volatile jobject *addr = (jobject *) ((char *) obj + offset);
*addr = value;
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* Sets next field with volatile write semantics. (See above
* about use of putOrderedObject.)
*/
final void setNext(HashEntry<K,V> n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
// Unsafe mechanics
static final sun.misc.Unsafe UNSAFE; //可以理解为一个指针
static final long nextOffset;//偏移量,可以简单的理解为内存地址
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();//获取这个节点对应的内存指针
Class k = HashEntry.class;//
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next")); //获取当前节点的next节点对于当前节点指针的偏移量
//通过UNSAFE中有方法直接能够获取到当前引用变量的初始内存地址
//通过初始内存地址和引用变量内部的局部变量的偏移量就可以通过Unsafe直接读取到对应的参数值
} catch (Exception e) {
throw new Error(e);
}
}
}
参考链接
https://blog.csdn.net/klordy_123/article/details/82933115
Java8 ConcurrentHashMap
底层依然由 Node数组+链表+红黑树 来实现的;
ConcurrentHashMap 使用 CAS 和 Synchronized 来保证线程安全。
put方法:
- 如果没有初始化就先调用initTable()方法来进行初始化过程
- 如果没有hash冲突就直接CAS插入
- 如果还在进行扩容操作就先进行扩容
- 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接 遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
- 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
get 方法:
计算 hash 值
根据 hash 值找到数组对应位置: (n – 1) & h
根据该位置处结点性质进行相应查找:
如果该位置为 null,那么直接返回 null 就可以了
如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法
如果以上 3 条都不满足,那就是链表,进行遍历比对即可