这里说的还是1.7的ConcurrentHashMap,在1.8中,ConcurrentHashMap已经不是基于segments实现了。

之前也知道ConcurrentHashMap是通过把锁加载各个segment上从而实现分段式锁来达到增加并发效率的,但是时间久了容易忘,这次再看了一下源码,记录一下以免再忘。为方便起见,把ConcurrentHashMap简称为CHM

首先看构造函数,有三个参数:initialCapacity,loadFactor,concurrencyLevel,前两个和HashMap是一样的,就是指定初始容量和扩容的比例参数,后面一个concurrencyLevel,从名字就可以看出来,这是指定并发数的。

 public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < 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<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}

根据concurencyLevel,CHM会设置一个SSize,代表着segment的数量,并且这个数量很有意思,它是一个不小于concurrencyLevel的最小的2的k次方。另外还有一个sshift,这个值就是k,通过它计算segmentMask=32-sshift,通过运算hash>>>segmentMask找到相对于的hash值的高sshift位,高sshift位所能表达的方位恰好是0到ssize-1,也就是segment的数量。所以用hash>>>segmentMask的来找到key对应的segment编号简直就是天才设计啊。通过看put函数,就会发现真的是这样:

 public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}

看第6行,j就是segment的编号,先通过Unsafe判断Segment存在与否,如果存在,就用ensureSegment(j)去获取这个segment。

再来看ensureSegment:

 private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}

我们再看segments,它是final的,所以它是多线程可见的,但是它的元素却不是。数组(或对象)为volatile,只能保证它的引用值是可见的,但是数组元素(或对象属性)却不是多线程可见的,这是java的设计缺陷。为了弥补这个缺陷,在上面的第5行用了 UNSAFE.getObjectVolatile(ss, u) 来补坑。

final Segment<K,V>[] segments

找到了segment之后,接下来就是调用segment的put方法把数据插入到segment里面了。看segment的put函数,第一行居然有一个trylock(),而且根据获取锁的结果进行了不同的操作。因为Segment类继承了ReetrantLock,所以锁的就是它本身。如果tryLock成功获取到写入锁,node就设为Null,然后进入正常的流程。如果获取锁失败,那么就要先进入scanAndLockForPut操作。

 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
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 {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
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;
}

我们接下来看tryLock失败之后会发生什么。我们知道,CHM在读取操作是不加锁,但是在写入操作的时候是一定要加锁的,但是呢,真的是一开始就加锁吗?仔细观察scanAndLockForPut函数,发现不是这样的。scanAndLockForPut函数有个循环,以自旋锁的方式一直在尝试获取锁,如果不成功就循环下去。但是就这样一直循环下去吗?也不是,可以看下面,有个retries参数,每循环一次,这个reties参数就加1。从19行到22行可知,当达到MAX_SCAN_RETRIES次数之后,就会直接调用lock(),自旋锁变成了重量级锁。而且我们可以看到,在自旋的过程中,scanAndForput并不是什么都没干的,它总是在干一件事儿:给node赋值。如果node为null,就生成新对象。如果找到了key相等的节点,则指向找到的节点。并且在这个过程中,每隔一步((retries & 1) == 0,不明白为什么要在偶数次检查)检查一次table是否已经变了,如果变了,则需要重新找node(把制为retries = -1;,是的重新进入retries < 0逻辑),开始重新计算retries。也就是说如果有线程改变了当前segment,则继续等待MAX_SCAN_RETRIES次。

 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; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
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) {//当达到MAX_SCAN_RETRIES次数之后,就会直接调用lock(),自旋锁变成了重量级锁。
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}

然后在回到put函数。put函数也很妙,我们直接以注解的方式来说明:

 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//先尝试获取锁,如果获取失败,则进入scanAndLockForPut函数,并在scanAndLockForPut函数中尝试以自旋的方式获取锁。
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
//计算hash桶的位置
int index = (tab.length - 1) & hash;
//获取到第一个节点
HashEntry<K,V> first = entryAt(tab, index);
//遍历hash桶中的HashEntry
for (HashEntry<K,V> e = first;;) {
if (e != null) {//如果没有遍历完
K k;
//如果找到了相同的key,则根据onlyIfAbsent判断是替换值还是不做任何操作,并且结束遍历
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 {//如果遍历到了头,则检查是否在scanAndLockForPut已经获取了node,如果是,则设置node的next为当前的first
if (node != null)
node.setNext(first);
else//如果scanAndLockForPut没有获取node,则新建node,注意,node的next还是first。
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
//如果超过了阈值,则先进行扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else//最后,把node放入table中。可见,新插入的node总是位于链表的最前端的
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}

可以看到,新加入的HashEntry总是位于链表的最前端。这是和HashMap的区别之一,HashMap新加入的节点是挂在链表的最后端的。

接下来看扩容,扩容位于函数rehash中,因为扩容函数比较复杂,所以我们依然用注解的方式逐条说明:

 private void rehash(HashEntry<K,V> node) {
/*
* Reclassify nodes in each list to new table. Because we
* are using power-of-two expansion, the elements from
* each bin must either stay at same index, or move with a
* power of two offset. We eliminate unnecessary node
* creation by catching cases where old nodes can be
* reused because their next fields won't change.
* Statistically, at the default threshold, only about
* one-sixth of them need cloning when a table
* doubles. The nodes they replace will be garbage
* collectable as soon as they are no longer referenced by
* any reader thread that may be in the midst of
* concurrently traversing table. Entry accesses use plain
* array indexing because they are followed by volatile
* table write.
*/
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
//重新计算扩容阈值
threshold = (int)(newCapacity * loadFactor);
//新建Table
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
//从新计算掩膜,掩膜的作用就是计算索引值,利用table长度是2的整数次方(k)这一特性,直接区hash值的低k位就是索引值
int sizeMask = newCapacity - 1;
//遍历处理table上的每一个hash桶
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
//如果当前hash桶不为空,则需要处理
HashEntry<K,V> next = e.next;
//计算新桶在新table中的位置
int idx = e.hash & sizeMask;
//如果当前hash桶只有一个节点,直接把他放到新桶中就可以了
if (next == null) // Single node on list
newTable[idx] = e;
//否则的话,就需要遍历当前桶的链表
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
//找到第一个后续元素的新位置都不变的节点,然后把这个节点当成头结点,直接把后续的整个链表都放入新table中
//因为table的容量总是2的k次方,而且每次扩容都是容量乘以2,也就是segmentMask会增加1位,那么,节点的新桶在
// 新table中的位置要么还是老位置,要么增加了一个oldCapacity,具体要看新增的这一位上key的hash值是否为1. for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
//直接把整个连续的位置不变的节点组成的链表加入到新桶中
newTable[lastIdx] = lastRun;
// Clone remaining nodes
//把剩下的节点(都在搬迁链表的前端)一个个放入到新桶中。
//同样,每次都是加入到桶的最前端
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);
}
}
}
}
//扩容完成之后,在新的table中插入要put的节点
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}

扩容后的节点的桶位置要么在和原来一样,要么就是增加一个oldCapacity。容量选为2的k次方的优势在rehash中再次得到体现。

最后再来看remove操作。因为1.7中HashEntry的next已经不是final而是volatile的了,所以remove操作改变比较大。在之前,因为无法改变一个HashEntry的next指针,所以只能把欲删除的节点之前的节点全部new一遍,并且会发生转向。而1.7中,可以直接改变next的值,从而实现节点的删除。

显式看remove函数。很简单,就是找到对应的segment,然后调用segment的remove函数。

 public boolean remove(Object key, Object value) {
int hash = hash(key);
Segment<K,V> s;
return value != null && (s = segmentForHash(hash)) != null &&
s.remove(key, hash, value) != null;
}

接下来看segment的remove函数,这个函数仍然是以注释的方式加以说明。

 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;
//并且值相等,或者传入的值为null(说明是删除指定的key),则删除
if (value == null || value == v || value.equals(v)) {
//如果是找到的节点头结点,则将next节点存入table中
if (pred == null)
setEntryAt(tab, index, next);
else
//否则让pred节点指向next节点,使当前节点删除
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
//否则什么都不做,直接跳出循环
break;
}
//继续找下一个节点
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}

发现remove函数里面也有一个类似于put函数中的scanAndLockForput函数的函数,叫做scanAndLock。但是很奇怪,这个函数只做自旋操作,却并不传递出任何只,其实完全可以在自旋的过程中找到prev和e的,而不是非要等到获取到锁之后再进行,不知道1.8中有没有改进(但是1.8已经没有segment了),有时间去看看1.8中怎么实现的。

 /**
* Scans for a node containing the given key while trying to
* acquire lock for a remove or replace operation. Upon
* return, guarantees that lock is held. Note that we must
* lock even if the key is not found, to ensure sequential
* consistency of updates.
*/
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;
}
}
}

再看clear。clear函数很简单,先是通过segments获取所有的segments,然后迭代删除:

 public void clear() {
final Segment<K,V>[] segments = this.segments;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> s = segmentAt(segments, j);
if (s != null)
s.clear();
}
}

每个segment的clear也很简单,就是获取table,然后让hash桶变成null:

 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();
}
}

再看size函数和isEmpty函数,也很有意思。他们都通过多次遍历segments,并比较各趟遍历的modCount之和,看是否变化,一次判断是否在调用size和isEmpty的时候是否发生了更改。

isEmpty之需循环遍历两趟,sum先在第一趟遍历中累加segment.modCount,在第二趟遍历时减掉各modCount,如果最终等于0,则说明两趟之间各modCount都没有变化,说明是准确的。如果这两趟遍历中,有一个segment不是Empty的,就会返回false。如果没有一个segment不是空的,最终就会返回true。但是很明显,这种方式有个缺点,他不能检测ABA问题,比如有一个segment增加了一个节点,有删除了一个节点,这是它还是空的,但是modCount却发生了变化,此时isEmpty就会返回错误的结果。另外,采用同样方式的还有containsValue操作,但是containsKey操作不是这样的,所以containsKey是不可靠的,很奇怪为什么要这么处理。

 public boolean isEmpty() {
/*
* Sum per-segment modCounts to avoid mis-reporting when
* elements are concurrently added and removed in one segment
* while checking another, in which case the table was never
* actually empty at any point. (The sum ensures accuracy up
* through at least 1<<31 per-segment modifications before
* recheck.) Methods size() and containsValue() use similar
* constructions for stability checks.
*/
long sum = 0L;
final Segment<K,V>[] segments = this.segments;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
if (seg.count != 0)
return false;
sum += seg.modCount;
}
}
if (sum != 0L) { // recheck unless no modifications
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
if (seg.count != 0)
return false;
sum -= seg.modCount;
}
}
if (sum != 0L)
return false;
}
return true;
}

size不是遍历两趟,而是多趟,如果有两趟之间modCount之和没有变化,则返回遍历的累加结果。否则继续遍历,当达到一定次数之后发现变化还是稳定不下来,就会对segment加锁遍历。

 public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}

温故而知新,再探ConcurrentHashMap的更多相关文章

  1. 【再探backbone 02】集合-Collection

    前言 昨天我们一起学习了backbone的model,我个人对backbone的熟悉程度提高了,但是也发现一个严重的问题!!! 我平时压根没有用到model这块的东西,事实上我只用到了view,所以昨 ...

  2. ViewPager+Fragment再探:和TAB滑动条一起三者结合

    Fragment前篇: <Android Fragment初探:静态Fragment组成Activity> ViewPager前篇: <Android ViewPager初探:让页面 ...

  3. 再探jQuery

    再探jQuery 前言:在使用jQuery的时候发现一些知识点记得并不牢固,因此希望通过总结知识点加深对jQuery的应用,也希望和各位博友共同分享. jQuery是一个JavaScript库,它极大 ...

  4. [老老实实学WCF] 第五篇 再探通信--ClientBase

    老老实实学WCF 第五篇 再探通信--ClientBase 在上一篇中,我们抛开了服务引用和元数据交换,在客户端中手动添加了元数据代码,并利用通道工厂ChannelFactory<>类创 ...

  5. Spark Streaming揭秘 Day7 再探Job Scheduler

    Spark Streaming揭秘 Day7 再探Job Scheduler 今天,我们对Job Scheduler再进一步深入一下,对一些更加细节的源码进行分析. Job Scheduler启动 在 ...

  6. 再探ASP.NET 5(转载)

    就在最近一段时间,微软又有大动作了,在IDE方面除了给我们发布了Viausl Studio 2013 社区版还发布了全新的Visual Studio 2015 Preview. Visual Stud ...

  7. 再探java基础——break和continue的用法

    再探java基础——break和continue的用法 break break可用于循环和switch...case...语句中. 用于switch...case中: 执行完满足case条件的内容内后 ...

  8. 第四节:SignalR灵魂所在Hub模型及再探聊天室样例

    一. 整体介绍 本节:开始介绍SignalR另外一种通讯模型Hub(中心模型,或者叫集线器模型),它是一种RPC模式,允许客户端和服务器端各自自定义方法并且相互调用,对开发者来说相当友好. 该节包括的 ...

  9. 深入出不来nodejs源码-内置模块引入再探

    我发现每次细看源码都能发现我之前写的一些东西是错误的,去改掉吧,又很不协调,不改吧,看着又脑阔疼…… 所以,这一节再探,是对之前一些说法的纠正,另外再缝缝补补一些新的内容. 错误在哪呢?在之前的初探中 ...

随机推荐

  1. windows phone之依赖属性(DependencyProperty)

    Windows Presentation Foundation (WPF) 提供了一组服务,这些服务可用于扩展公共语言运行时 (CLR) 属性的功能,这些服务通常统称为 WPF 属性系统.由 WPF ...

  2. .NET Core Api 集成 swagger

    废话不多讲 第一步  当然是要通过 NuGet 安装第三方插件 swagger 程序包管理器控制台,安装命令:Install-Package Swashbuckle.AspNetCore -Pre 第 ...

  3. MVC 路由调试工具-RouteDebugger

    MVC  路由调试工具-RouteDebugger 方案一: 在程序包控制台中执行命令 PM> Install-Package routedebugger 自动会在你的项目webconfig中& ...

  4. 大数据技术之_11_HBase学习_01_HBase 简介+HBase 安装+HBase Shell 操作+HBase 数据结构+HBase 原理

    第1章 HBase 简介1.1 什么是 HBase1.2 HBase 特点1.3 HBase 架构1.3 HBase 中的角色1.3.1 HMaster1.3.2 RegionServer1.3.3 ...

  5. pch文件配置出现 Expected unqualified-id 和 Unkown type name 'NSString'

    1.发生的现象 之前代码还是没有报错的,由于某些代码比较常用,就打算配置一个pch文件引入常用的文件 但是引入的时候就出现了报错 2.原因与解决办法 2.1 原因 你引入的文件可能使用到OC与C++混 ...

  6. Flutter的使用教学笔记

    QQ交流群 Flutter 程序开发群:766307130 教程 官方实战 使用Flutter 构建精美的页面 云在千峰 博主一直是从事 Android 开发的,所以主要从 Android 技术角度来 ...

  7. WCF进阶(一)——概述

    前言 时间的朋友>里面几句特别有预见性的话来说明当今儿世界互联网发展趋势: 市场上有一种叫做"父爱算法"的需求.将会诞生很多伟大的公司.背后的精神就是六个字:你不用懂,听我的 ...

  8. Array数组结构底层实现复习

    Array数组结构底层实现复习 内容待总结: size capacity length

  9. swift pod 第三方库异常的处理

    Xcode8—Swift开发使用Cocoapods引入第三方库异常处理方法 参考:  http://www.jianshu.com/p/23f13be525a0 //podfile文件如下 platf ...

  10. python语法学习面向对象之继承

    python语法学习面向对象之继承 转载自:http://www.cnblogs.com/Joans/archive/2012/11/09/2757368.html 只要涉及到面向对象,”类“是必须出 ...