ConcurrentHashMap通过将完整的表分成若干个segment的方式实现锁分离,每个segment都是一个独立的线程安全的Hash表,当需要操作数据时,HashMap通过Key的hash值和segment数量来路由到某个segment,剩下的操作交给segment来完成。

下面来看下segment的实现:

segment是一类特殊的hash表,继承了ReentrantLock类实现锁功能

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
/*
1.segment的读操作不需要加锁,但需要volatile读
2.当进行扩容时(调用reHash方法),需要拷贝原始数据,在拷贝数据上操作,保证在扩容完成前读操作仍可以在原始数据上进行。
3.只有引起数据变化的操作需要加锁。
4.scanAndLock(删除、替换)/scanAndLockForPut(新增)两个方法提供了获取锁的途径,是通过自旋锁实现的。
5.在等待获取锁的过程中,两个方法都会对目标数据进行查找,每次查找都会与上次查找的结果对比,虽然查找结果不会被调用它的方法使用,但是这样做可以减少后续操作可能的cache miss。
*/ private static final long serialVersionUID = 2249069246763182397L; /*
自旋锁的等待次数上限,多处理器时64次,单处理器时1次。
每次等待都会进行查询操作,当等待次数超过上限时,不再自旋,调用lock方法等待获取锁。
*/
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
/*
segment中的hash表,与hashMap结构相同,表中每个元素都是一个链表。
*/
transient volatile HashEntry<K,V>[] table; /*
表中元素个数
*/
transient int count; /*
记录数据变化操作的次数。
这一数值主要为Map的isEmpty和size方法提供同步操作检查,这两个方法没有为全表加锁。
在统计segment.count前后,都会统计segment.modCount,如果前后两次值发生变化,可以判断在统计count期间有segment发生了其它操作。
*/
transient int modCount; /*
容量阈值,超过这一数值后segment将进行扩容,容量变为原来的两倍。
threshold = loadFactor*table.length
*/
transient int threshold; final float loadFactor; Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
/*
onlyIfAbsent:若为true,当key已经有对应的value时,不进行替换;
若为false,即使key已经有对应的value,仍进行替换。 关于put方法,很重要的一点是segment最大长度的问题:
代码 c > threshold && tab.length < MAXIMUM_CAPACITY 作为是否需要扩容的判断条件。
扩容条件是node总数超过阈值且table长度小于MAXIMUM_CAPACITY也就是2的30次幂。
由于扩容都是容量翻倍,所以tab.length最大值就是2的30次幂。此后,即使node总数超过了阈值,也不会扩容了。
由于table[n]对应的是一个链表,链表内元素个数理论上是无限的,所以segment的node总数理论上也是无上限的。
ConcurrentHashMap的size()方法考虑到了这个问题,当计算结果超过Integer.MAX_VALUE时,直接返回Integer.MAX_VALUE. */
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//tryLock判断是否已经获得锁.
//如果没有获得,调用scanAndLockForPut方法自旋等待获得锁。
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
//计算key在表中的下标
int index = (tab.length - 1) & hash;
//获取链表的第一个node
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
//链表下一个node不为空,比较key值是否相同。
//相同的,根据onlyIfAbsent决定是否替换已有的值
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
//链表遍历到最后一个node,仍没有找到key值相同的.
//此时应当生成新的node,将node的next指向链表表头,这样新的node将处于链表的【表头】位置
if (node != null)
//scanAndLockForPut当且仅当hash表中没有该key值时
//才会返回新的node,此时node不为null
node.setNext(first);
else
//node为null,表明scanAndLockForPut过程中找到了key值相同的node
//可以断定在等待获取锁的过程中,这个node被删除了,此时需要新建一个node
node = new HashEntry<K,V>(hash, key, value, first);
//添加新的node涉及到扩容,当node数量超过阈值时,调用rehash方法进行扩容,并将新的node加入对应链表表头;
//没有超过阈值,直接加入链表表头。
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
} /*
hash表容量翻倍,将需要添加的node添加到扩容后的表中。
hash表默认初始长度为16,实际长度总是2的n次幂。
设当前table长度为S,根据key的hash值计算table中下标index的公式:
扩容前:oldIndex = (S-1)&hash
扩容后:newIndex = (S<<1-1)&hash
扩容前后下标变化:newIndex-oldIndex = S&hash
所以,扩容前后node所在链表在table中的下标要么不变,要么右移2的幂次。
根据本方法官方注释说明,大约六分之一的node需要复制操作。 对于每个链表,处理方法如下: 步骤一:对于链表中的每个node,计算node和node.next的新下标,如果它们不相等,记录最后一次出现这种情况时的node.next,记为nodeSpecial。
这一部分什么意思呢,假设table[n]所在的链表共有6个node,计算它们的新下标:
情况1:若计算结果为0:n,1:n+S,2:n,3:n+2,4:n,5:n,那么我们记录的特殊node编号为4;
情况2:若计算结果为0:n,1:n+S,2:n,3:n+2,4:n+4,5:n+8,那么我们记录的特殊node编号为5;
情况3:若计算结果为0:n,1:n,2:n,3:n,4:n,5:n,特殊node为0;
情况4:若计算结果为0:n+S,1:n+S,2:n+S,3:n+S,4:n+S,5:n+S,特殊node为0。
很重要的一点,由于新下标只可能是n或n+S,因此这两个位置的链表中不会出现来自其它链表的node。
对于情况3,令table[n]=node0,进入步骤三;
对于情况4,令table[n+S]=node0,进入步骤三;
对于情况1,令table[n]=node4,进入步骤二;
对于情况2,令table[n+S]=node3,进入步骤二。 步骤二:从node0遍历至nodeSpecial的前一个node,对于每一个node,调用HashEntry构造方法复制这个node,放入对应的链表。 步骤三:计算需要新插入的node的下标index,同样令node.next=table[index],table[index]=node,将node插入链表表头。 通过三步完成了链表的扩容和新node的插入。 在理解这一部分代码的过程中,牢记三点:
1.调用rehash方法的前提是已经获得了锁,所以扩容过程中不存在其他线程修改数据;
2.新的下标只有两种情况,原始下标n或者新下标n+S;
3.通过2可以推出,原表中不在同一链表的node,在新表中仍不会出现在同一链表中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
//拷贝table,所有操作都在oldTable上进行,不会影响无需获得锁的读操作
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;//容量翻倍
threshold = (int)(newCapacity * loadFactor);//更新阈值
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;//新的table下标,定位链表
if (next == null)
//链表只有一个node,直接赋值
newTable[idx] = e;
else {
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
//这里获取特殊node
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//步骤一中的table[n]赋值过程
newTable[lastIdx] = lastRun;
// 步骤二,遍历剩余node,插入对应表头
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
//步骤三,处理需要插入的node
int nodeIndex = node.hash & sizeMask;
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
//将扩容后的hashTable赋予table
table = newTable;
} /*
put方法调用本方法获取锁,通过自旋锁等待其他线程释放锁。
变量retries记录自旋锁循环次数,当retries超过MAX_SCAN_RETRIES时,不再自旋,调用lock方法等待锁释放。
变量first记录hash计算出的所在链表的表头node,每次循环结束,重新获取表头node,与first比较,如果发生变化,说明在自旋期间,有新的node插入了链表,retries计数重置。
自旋过程中,会遍历链表,如果发现不存在对应key值的node,创建一个,这个新node可以作为返回值返回。
根据官方注释,自旋过程中遍历链表是为了缓存预热,减少hash表经常出现的cache miss
*/
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; //自旋次数计数器
while (!tryLock()) {
HashEntry<K,V> f;
if (retries < 0) {
if (e == null) {
//链表为空或者遍历至链表最后一个node仍没有找到匹配
if (node == null)
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
//比较first与新获得的链表表头node是否一致,如果不一致,说明该链表别修改过,自旋计数重置
e = first = f;
retries = -1;
}
}
return node;
} /*
remove,replace方法会调用本方法获取锁,通过自旋锁等待其他线程释放锁。
与scanAndLockForPut机制相似。
*/
private void scanAndLock(Object key, int hash) {
// similar to but simpler than scanAndLockForPut
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
int retries = -1;
while (!tryLock()) {
HashEntry<K,V> f;
if (retries < 0) {
if (e == null || key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
} /*
删除key-value都匹配的node,删除过程很简单:
1.根据hash计算table下标index。
2.根据index定位链表,遍历链表node,如果存在node的key值和value值都匹配,删除该node。
3.令node的前一个节点pred的pred.next = node.next。
*/
final V remove(Object key, int hash, Object value) {
//获得锁
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
/*
找到hash表中key-oldValue匹配的node,替换为newValue,替换过程与replace方法类似,不再赘述了。
*/
final boolean replace(K key, int hash, V oldValue, V newValue) {
if (!tryLock())
scanAndLock(key, hash);
boolean replaced = false;
try {
HashEntry<K,V> e;
for (e = entryForHash(this, hash); e != null; e = e.next) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
if (oldValue.equals(e.value)) {
e.value = newValue;
++modCount;
replaced = true;
}
break;
}
}
} finally {
unlock();
}
return replaced;
} final V replace(K key, int hash, V value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V> e;
for (e = entryForHash(this, hash); e != null; e = e.next) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
e.value = value;
++modCount;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
/*
清空segment,将每个链表置为空,count置为0,剩下的工作交给GC。
*/
final void clear() {
lock();
try {
HashEntry<K,V>[] tab = table;
for (int i = 0; i < tab.length ; i++)
setEntryAt(tab, i, null);
++modCount;
count = 0;
} finally {
unlock();
}
}
}

以上就是ConcurrentHashMap中关于segment部分的源码。可以看出,关于修改操作的线程安全已经被封装在segment中,甚至在ConcurrentHashMap中都不需要关心锁的问题。可以说,segment是整个ConcurrentHashMap的核心和关键。

版权声明:本文为博主原创文章,未经博主允许不得转载.

ConcurrentHashMap.Segment源码解析的更多相关文章

  1. ConcurrentHashMap源码解析(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 注:在看这篇文章之前,如果对HashMap的层不清楚的话,建议先去看看HashMap源码解析. http:/ ...

  2. 第二章 ConcurrentHashMap源码解析

    注:在看这篇文章之前,如果对HashMap的层不清楚的话,建议先去看看HashMap源码解析. http://www.cnblogs.com/java-zhao/p/5106189.html 1.对于 ...

  3. Java泛型底层源码解析--ConcurrentHashMap(JDK1.7)

    1. Concurrent相关历史 JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全 ...

  4. Java之ConcurrentHashMap源码解析

    ConcurrentHashMap源码解析 目录 ConcurrentHashMap源码解析 jdk8之前的实现原理 jdk8的实现原理 变量解释 初始化 初始化table put操作 hash算法 ...

  5. ConcurrentHashMap源码解析,多线程扩容

    前面一篇已经介绍过了 HashMap 的源码: HashMap源码解析.jdk7和8之后的区别.相关问题分析 HashMap并不是线程安全的,他就一个普通的容器,没有做相关的同步处理,因此线程不安全主 ...

  6. Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析

    目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...

  7. 源码解析之ConcurrentHashmap

    ConcurrentHashmap算是我看的集合源码里最难理解的了(当然ConcurrentLinkedList虽然代码少但理解起来也累),在Java1.8版本中DougLea大师巧通过妙地代码把锁粒 ...

  8. ConcurrentHashMap源码解析 JDK8

    一.简介 上篇文章详细介绍了HashMap的源码及原理,本文趁热打铁继续分析ConcurrentHashMap的原理. 首先在看本文之前,希望对HashMap有一个详细的了解.不然看直接看Concur ...

  9. Google guava cache源码解析1--构建缓存器(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 1.guava cache 当下最常用最简单的本地缓存 线程安全的本地缓存 类似于ConcurrentHas ...

随机推荐

  1. php class 访问控制

    属性(attribute ) 必须声明访问控制类型 类型: public 公用 protected 受保护的 private  私有的 public 类型的属性 可以在外部访问 protected 及 ...

  2. apollo 项目配置中心开源框架部署

    apollo 于我带来的好处 1. 项目之前的配置信息全部都在 resources 目录下,当然这里我使用的是 Spring Boot 搭建的项目.使用 apollo 后,配置信息全部转移到 apol ...

  3. Shutdown SpringBoot App

    文章目录 Shutdown Endpoint close Application Context 退出SpringApplication 从外部程序kill App Shutdown SpringBo ...

  4. SpringBoot 集成Swagger2自动生成文档和导出成静态文件

    目录 1. 简介 2. 集成Swagger2 2.1 导入Swagger库 2.2 配置Swagger基本信息 2.3 使用Swagger注解 2.4 文档效果图 3. 常用注解介绍 4. Swagg ...

  5. SSH公钥登录和RSA非对称加密

    SSH登录方式 接触过Linux服务器的同学肯定用过SSH协议登录系统,通常SSH协议都有两种登录方式:密码口令登录和公钥登陆. 一.密码口令(类似于账号密码登录) 1.客户端连接服务器,服务器把公钥 ...

  6. sed命令的正则表达式实践

    1. 取系统ip [root@oldboy logs]# ifconfig eth3 eth3 Link encap:Ethernet HWaddr 08:00:27:4C:6F:AD inet ad ...

  7. nodejs实现定时爬取微博热搜

    The summer is coming " 我知道,那些夏天,就像青春一样回不来. - 宋冬野 青春是回不来了,倒是要准备渡过在西安的第三个夏天了. 废话 我发现,自己对 coding 这 ...

  8. CSS常用遮罩层

    为什么80%的码农都做不了架构师?>>>   CSS常用遮罩层 应用场景: 上传了一张图片,鼠标移入到图片上的时候显示遮罩层,并且提示点击删除. 通过改变遮罩层的透明度来实现显示隐藏 ...

  9. Retrofit的文件上传和进度提示

    2019独角兽企业重金招聘Python工程师标准>>> 1.写一个上传监听的接口: /** * Created by Zzm丶Fiona on 2017/7/31. */ publi ...

  10. 第七周CorelDRAW课总结

    1.这节课学到了什么知识? "交互式透明工具""交互式阴影工具"以及"交互式调和工具"制作水晶表情包. 2.有什么心得体会? 无 3.存在的 ...