解读该源码最好先了解HashMap的源码
ConcurrentHashMap()
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
这个构造函数要看一下:
ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)//判断参数是否合法
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)//concurrentLevel 代表的是同时操作map的线程数
concurrencyLevel = MAX_SEGMENTS;//MAX_SEGMENTS 多核cpu,这个值为64 单核 这个值为1
// Find power-of-two sizes best matching arguments
int sshift = 0;//记录ssize左移的次数
int ssize = 1;//Segment数组的真正大小
while (ssize < concurrencyLevel) {//将ssize改为不小于concurrencyLevel的2的幂次方
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)//不能超过最大容量 1<<30
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;// 计算每个Segment中HashEntry数组的大小(注意这个c不是真正的数组大小)
if (c * ssize < initialCapacity)//这一步是向上取整操作 例如 9/8=1 1*8 <9 1++ -> 2
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;//这个cap才是HashEntry数组的真正大小,最小是2
while (cap < c)//找到一个2的幂次方 不小于c的 最小的数
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);//定义Segment[0]
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];//定义Segment数组
UNSAFE.putOrderedObject(ss, SBASE, s0); // 将s0 写入到 ss的第0个位置
this.segments = ss;//将刚刚定义的Segment数组赋值给 segments
}
看完到这里,要明白一个事情:
ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<>(9,0.75f,8);
使用这个构造函数,Segment数组大小是多少?
答案 8
每个Segment中,HashEntry数组大小是多少?
答案 2
这里要解释一下,为什么初始化只声明了一个s0,而没有将其他的也全部初始化好呢?
答案 因为没必要全部初始化好,其他的还有用到,用到的时候在创建Segment对象,可能有人要问了,s0这里其实也没有用到, 为什么要声明,这是因为 每次创建一个Segment对象要计算好几个值,初始化ConcurrentHashMap的时候初始化了一个s0,只有再要初始化的Segment对象的时候,就拿s0当模板直接照搬参数就行,这样就会快一点。
这个方法干什么的不用多解释了吧~
put(K key, V value)
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)//这里提醒一下哦,HashMap是可以put(null,null)的,但是ConcurrentHashMap.put(null,null)是不可以的
throw new NullPointerException();
int hash = hash(key);//计算key的hash值
int j = (hash >>> segmentShift) & segmentMask;//这行代码就是计算在Segment数组中的下标
if ((s = (Segment<K,V>)UNSAFE.getObject // 这一步是取出segments中下标为j的segment对象,赋值给s
(segments, (j << SSHIFT) + SBASE)) == null) // 如果为空,则说说明这个地方还没放过内容
s = ensureSegment(j);//声明一个segment对象 下面有讲解这个方法
return s.put(key, hash, value, false);//真正的放入 下面有讲解这个方法
}
下面这个方法,干的事情,就几行代码:
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
UNSAFE.compareAndSwapObject(ss, u, null, seg = s)
return seg;
为什么写了看起来还挺长的挺多代码呢,这是因为要保证线程安全。
ensureSegment(int k)
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;//首先获取segment数组
long u = (k << SSHIFT) + SBASE; // u 其实就是偏移下标,这里可以理解为用k算出来的真正的下标:segment[index]
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {//将segment[u]取出,注意,这里是getObjectVolatile这个方法, 也就是说,别的线程要是修改了,segment[u],这个线程是可见的
Segment<K,V> proto = ss[0]; // proto 取出ss[0],作为segment对象模板
int cap = proto.table.length;//拿到Segment中HashEntry数组的长度
float lf = proto.loadFactor;//拿到负载因子
int threshold = (int)(cap * lf);//拿到阈值 ---- 这里是jdk1.7,没有红黑树,这个阈值是用于判断是否需要扩容重hash的
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];//构建HashEntry数组
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//二次检查是否segment[u]是否为null(即:在做上面那些操作时,看看是否有别的线程操作过segment[u])
== null) {
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);//去构建Segment对象
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))//再次检查segment[u]是否为null,注意,这里是while,之前的if,也是起到如果下面操作失败,再次检查的作用
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))//CAS进行赋值,这里的比较并交换在CPU里面就是一条指令,保证原子性的,不存在 比较完,去交换的间隙 别的线程修改了这个值的可能
break;//设置完,跳出
}
}
}
return seg;//返回segment对象,注意,这里返回的seg可能是自己new的,也可能是别的线程new的
}
下面这个方法是ConcurrentHashMap真正开始去放入key-value,前面一直在做准备工作,segment对象初始化,保证拿到的segment对象肯定不是null。
前情:
ConcurrentHashMap和HashMap相比,最直白的最大的特点就是并发安全对吧,那么我们在加入元素的时候,先进行上锁,再进行放入元素,然后再解锁,是这样一个思路吧,好的,我们再看一下下面的代码,是不是就是这个思路呢。。
首先 tryLock() 然后进行了一些操作,最后你看finally里面是不是就是解锁。大致是这么个思路,接下来,开始逐行看。
对了,再提一个知识点:
我们知道 Segment 是继承了 ReentrantLock的,那么就可以调用 trylock() 和 lock()
这两个方法的区别在于:trylock是不阻塞的 lock是阻塞的
s.put(key, hash, value, false);
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null ://首先调用tryLock,尝试获取锁 获取到了就返回true
scanAndLockForPut(key, hash, value);//获取锁失败就调用scanAndLockForPut方法 下面会讲解该方法
V oldValue;
try {//此时拿到锁的线程开始工作
HashEntry<K,V>[] tab = table;//拿到 HashEntry数组
int index = (tab.length - 1) & hash;//计算好下标
HashEntry<K,V> first = entryAt(tab, index);//这个方法就简单理解为 从tab中拿到index位置的第一个元素
for (HashEntry<K,V> e = first;;) {//e 记录刚刚拿到的第一个元素 ------ 这里是个死循环
if (e != null) {//判断是否被初始化过
K k;
if ((k = e.key) == key ||//判断key是否相等
(e.hash == hash && key.equals(k))) {//判断Hash,再判断equals,这个顺序也是有讲究的
oldValue = e.value;//找到了同样的key ,记录老value
if (!onlyIfAbsent) {
e.value = value;//更新value为我们的value
++modCount;//修改次数 +1
}
break;
}
e = e.next;//更新e的位置
}
else {//此时说明 e到了链表尾部 或者 e没被初始化过
if (node != null)//不为 null 说明之前做了准备工作
node.setNext(first);//头插法
else//没进行准备工作
node = new HashEntry<K,V>(hash, key, value, first);//准备好HashEntry节点
int c = count + 1;//记录 链表长度
if (c > threshold && tab.length < MAXIMUM_CAPACITY)//判断是否超过阈值
rehash(node);//扩容,进行 重复hash 这个方法就不看了,因为1.8超过阈值采用的是扩容和红黑树
else
setEntryAt(tab, index, node);//将node放置在tab的index下标位置上
++modCount;//修改次数 +1
count = c; //更新count值
oldValue = null;// 此时oldValue应该为null
break;
}
}
} finally {
unlock();//释放锁
}
return oldValue;//返回老value 如果没有key冲突,则是null
}
下面这个是在获取锁失败的时候调用,那么想想,获取锁失败,应该干嘛呢?这是ConcurrentHashMap为什么比HashTable效率高的原因所在了,HashTable想必大家应该知道,就是在HashMap的方法上用synconized关键字修饰,换成白话就是
HashTable获取锁失败,那么就进入阻塞状态,就什么都不干,一直等着
但是获取锁失败其实还可以去做别的事情,ConcurrentHashMap就是这样做的
ConcurrentHashMap获取锁失败,做了一些准备工作。初始化HashEntry,也就是将即将要加进去的元素构建成一个HashEntry对象
scanAndLockForPut(K key, int hash, V value)
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);//简单讲 拿到segment中HashEntry数组中通过hash计算的那个下标下的第一个节点
HashEntry<K,V> e = first;//辅助变量 赋值一份当前取出来的 内容
HashEntry<K,V> node = null;//辅助变量
int retries = -1; // 用于记录获取锁的次数
while (!tryLock()) {// 获取锁,如果获取失败,就会去进行一些准备工作, 和HashTable等待的区别
HashEntry<K,V> f; // 辅助变量用于重复检查 从segment中HashEntry数组中之前取出来的第一个节点是否还是我们之前取得那个
if (retries < 0) {//准备工作,因为准备工作也不需要每次循环都去做对吧,最好的预期,准备工作,做一次就够了
if (e == null) {//判断segment中的HashEntry是不是还没被初始化
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);//将我们即将插进去的元素,构建成HashEntry对象
retries = 0;//将retries赋值为0,不让准备工作重复执行
}
else if (key.equals(e.key))//判断 key是否有重复的
retries = 0;//如果有,也即找到了位置了,也不用再做准备工作了
else
e = e.next;//遍历segment中的HashEntry链表
}
else if (++retries > MAX_SCAN_RETRIES) {//判断最大的尝试获取锁的次数 同样,多核为64次 单核1次
lock();//超过了最大次数,阻塞锁 ---- 避免cpu空转
break;
}
else if ((retries & 1) == 0 &&//偶数次数才进行后面的判断
(f = entryForHash(this, hash)) != first) {//重复检查segment第一个节点是不是被修改了
e = first = f; // 此时为 别的线程修改了该segment的节点,重新赋值e first为最初值,和第一 第二行代码一样的效果
retries = -1;//修改尝试标志 这样就会再去做准备工作
}
}
return node;//将准备工作制作好的节点返回
}
注意看这个地方:
else if ((retries & 1) == 0 &&//偶数次数才进行后面的判断
(f = entryForHash(this, hash)) != first) {//重复检查segment第一个节点是不是被修改了
源码是如何判断segment中某个地方被动过的呢?
为什么 f = entryForHash(this, hash)) != first 这行的意思想必大家能看出来这样写的意思吧,因为这里是基于jdk1.7的,此时ConcurrentHashMap是头插法,所以只要比头节点即可。如果是1.8,那就不是比头节点了。
entryForHash(Segment<K,V> seg, int h)
static final <K,V> HashEntry<K,V> entryForHash(Segment<K,V> seg, int h) {
HashEntry<K,V>[] tab;//赋值变量,用于记录当前 segment中的HashEntry数组
return (seg == null || (tab = seg.table) == null) ? null ://判断当前HashEntry数组是否为null 如果是null 则直接返回null
(HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);//通过hash计算下标 然后取出 tab中对应下标的第一个HashEntry节点
}
再次说明哦,这里是基于1.7的。
如有错误,欢迎指出哦。
本文深入解析了ConCurrentHashMap在JDK 1.7中的实现原理,包括构造函数、put方法等核心部分,并详细介绍了线程安全的实现机制。
1450

被折叠的 条评论
为什么被折叠?



