(1) 背景
HashMap死循环:HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry.
HashTable效率低下:HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下.因为当一个线程访问HashTable的同步方法,其它线程也访问HashTable的同步方法时,会进入阻塞或轮询状态.如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法获取元素,所以竞争越激烈效率越低.
(2) 简介
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么多线程访问容器里不同的数据段时,线程间不会存在竞争,从而可以有效提高并发访问效率,这就是ConcurrentHash所使用的锁分段技术.首先将数据分成一段一段地储存,然后给每一段配一把锁,当一个线程占用锁访问其中一段数据时,其它段的数据也能被其它线程访问.创新互联建站专注为客户提供全方位的互联网综合服务,包含不限于成都网站建设、成都网站制作、南木林网络推广、微信平台小程序开发、南木林网络营销、南木林企业策划、南木林品牌公关、搜索引擎seo、人物专访、企业宣传片、企业代运营等,从售前售中售后,我们都将竭诚为您服务,您的肯定,是我们最大的嘉奖;创新互联建站为所有大学生创业者提供南木林建站搭建服务,24小时服务热线:18982081108,官方网址:www.cdcxhl.com
结构
ConcurrentHashMap是由Segments数组结构和HashEntry数组结构组成.Segment是一种可重入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的色;HashEntry则用于存储键值对数据.一个ConcurrentHashMap里包含一个Segment组.Segment的结构和HashMap类似,是一种数组加链表的结构.一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护者一个HashEntry数组里面的元素,当对HashEntry数组的数据进行修改时,必须先获得与它对应的Segment锁,如下图所示.
基本成员
default_initial_capacitymap默认容量,必须是2的冥
/**
* 默认的初始容量 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
default_load_factor默认负载因子(存储的比例)
/**
* 默认的负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
default_concurrency_level默认并发数量,segments数组量(ps:初始化后不能修改)
/**
* 默认的并发数量,会影响segments数组的长度(初始化后不能修改)
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
maximum_capacitymap最大容量
/**
* 最大容量,构造ConcurrentHashMap时指定的值超过,就用该值替换
* ConcurrentHashMap大小必须是2^n,且小于等于2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
min_segment_table_capacityHashEntry[]默认容量
/**
* 每个segment中table数组的长度,必须是2^n,至少为2
*/
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
max_segments最大并发数,segments数组最大量
/**
* 允许最大segment数量,用于限定concurrencyLevel的边界,必须是2^n
*/
static final int MAX_SEGMENTS = 1 << 16;
retries_before_lock重试次数,在加锁之前
/**
* 非锁定情况下调用size和contains方法的重试次数,避免由于table连续被修改导致无限重试
*/
static final int RETRIES_BEFORE_LOCK = 2;
segmentMask计算segment位置的掩码(segments.length-1)
/**
* 用于segment的掩码值,用于与hash的高位进行取&
*/
final int segmentMask;
segmentShift
/**
* 用于算segment位置时,hash参与运算的位数
*/
final int segmentShift;
segmentssegment数组
/**
* segments数组
*/
final Segment[] segments;
HashEntry存储数据的链式结构
static final class HashEntry {
// hash值
final int hash;
// key
final K key;
// 保证内存可见性,每次从内存中获取
volatile V value;
volatile HashEntry next;
HashEntry(int hash, K key, V value, HashEntry next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/**
* 使用volatile语义写入next,保证可见性
*/
final void setNext(HashEntry n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
Segment继承ReentrantLock锁,用于存放HashEntry[]
static final class Segment extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
/**
* 对segment加锁时,在阻塞之前自旋的次数
*
*/
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
/**
* 每个segment的HashEntry table数组,访问数组元素可以通过entryAt/setEntryAt提供的volatile语义来完成
* volatile保证可见性
*/
transient volatile HashEntry[] table;
/**
* 元素的数量,只能在锁中或者其他保证volatile可见性之间进行访问
*/
transient int count;
/**
* 当前segment中可变操作发生的次数,put,remove等,可能会溢出32位
* 它为chm isEmpty() 和size()方法中的稳定性检查提供了足够的准确性.
* 只能在锁中或其他volatile读保证可见性之间进行访问
*/
transient int modCount;
/**
* 当table大小超过阈值时,对table进行扩容,值为(int)(capacity *loadFactor)
*/
transient int threshold;
/**
* 负载因子
*/
final float loadFactor;
/**
* 构造方法
*/
Segment(float lf, int threshold, HashEntry[] tab) {
this.loadFactor = lf;
this.threshold = threshold;
this.table = tab;
}
/**
* ConcurrentHashMap 构造方法
* @param initialCapacity 初始化容量
* @param loadFactor 负载因子
* @param concurrencyLevel 并发segment,segments数组的长度
*/
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 大于最大segments容量,取最大容量
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
// 2^sshift = ssize 例如:sshift = 4,ssize = 16
// 根据concurrencyLevel计算出ssize为segments数组的长度
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) { // 第一次 满足
++sshift; // 第一次 1
ssize <<= 1; // 第一次 ssize = ssize << 1 (1 * 2^1)
}
// segmentShift和segmentMask的定义
this.segmentShift = 32 - sshift; // 用于计算hash参与运算位数
this.segmentMask = ssize - 1; // segments位置范围
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 计算每个segment中table的容量
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// HashEntry[]默认 容量
int cap = MIN_SEGMENT_TABLE_CAPACITY;
// 确保cap是2^n
while (cap < c)
cap <<= 1;
// create segments and segments[0]
// 创建segments并初始化第一个segment数组,其余的segment延迟初始化
Segment s0 =
new Segment(loadFactor, (int)(cap * loadFactor),
(HashEntry[])new HashEntry[cap]);
Segment[] ss = (Segment[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
无参构造使用默认参数
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
基本方法
一些UNSAFE方法
HashEntry
setNext
/**
* 使用volatile语义写入next,保证可见性
*/
final void setNext(HashEntry n) {
UNSAFE.putOrderedObject(this, nextOffset, n);
}
entryAtget HashEntry
/**
* 获取给定table的第i个元素,使用volatile读语义
*/
static final HashEntry entryAt(HashEntry[] tab, int i) {
return (tab == null) ? null :
(HashEntry) UNSAFE.getObjectVolatile
(tab, ((long)i << TSHIFT) + TBASE);
}
setEntryAtset HashEntry
/**
* 设置给定的table的第i个元素,使用volatile写语义
*/
static final void setEntryAt(HashEntry[] tab, int i,
HashEntry e) {
UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}
put插入元素
执行流程分析
(1) map的put方法就做了三件事情,找出segments的位置;判断当前位置有没有初始化,没有就调用ensureSegment()方法初始化;然后调用segment的put方法.
(2) segment的put方法.,获取当前segment的锁,成功接着执行,失败调用scanAndLockForPut方法自旋获取锁,成功后也是接着往下执行.
(3) 通过hash计算出位置,获取节点,找出相同的key和hash替换value,返回.没有找到相同的,设置找出的节点为当前创建节点的next节点,设置创建节点前,判断是否需要扩容,需要调用扩容方法rehash();不需要,设置节点,返回,释放锁.
/**
* map的put方法,定位segment
*/
public V put(K key, V value) {
Segment s;
// value不能为空
if (value == null)
throw new NullPointerException();
// 获取hash
int hash = hash(key);
// 定位segments 数组的位置
int j = (hash >>> segmentShift) & segmentMask;
// 获取这个segment
if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
// 为null 初始化当前位置的segment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
/**
* put到table方法
*/
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 是否获取锁,失败自旋获取锁(直到成功)
HashEntry node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry[] tab = table;
// 定义位置
int index = (tab.length - 1) & hash;
// 获取第一个桶的第一个元素
// entryAt 底层调用getObjectVolatile 具有volatile读语义
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) { // 证明链式结构有数据 遍历节点数据替换,直到e=null
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) { // 找到了相同的key
oldValue = e.value;
if (!onlyIfAbsent) { // 默认值false
e.value = value; // 替换value
++modCount;
}
break; // 结束循环
}
e = e.next;
}
else { // e=null (1) 之前没有数据 (2) 没有找到替换的元素
// node是否为空,这个获取锁的是有关系的
// (1) node不为null,设置node的next为first
// (2) node为null,创建头节点,指定next为first
if (node != null)
// 底层使用 putOrderedObject 方法 具有volatile写语义
node.setNext(first);
else
node = new HashEntry(hash, key, value, first);
int c = count + 1;
// 扩容条件 (1)entry数量大于阈值 (2) 当前table的数量小于最大容量 满足以上条件就扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
// 扩容方法,方法里面具体讲
rehash(node);
else
// 给table的index位置设置为node,
// node为头结点,原来的头结点first为node的next节点
// 底层也是调用的 putOrderedObject 方法 具有volatile写语义
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
解释下(hash >>> segmentShift) & segmentMask定位segment位置(个人理解)
ensureSegment初始化segment方法
执行流程
(1) 计算位置,使用UNSAFE的方法判断当前位置有没有初始化,然后使用segmets[0]的模板创建一个新的HashEntry[],再次判断当前位置有没有初始化,可能存在多线程同时初始化,然后创建一个新的segment,最后使用自旋cas设置新的segment的位置,保证只有一个线程初始化成功.
/**
*
* @param k 位置
* @return segments
*/
private Segment ensureSegment(int k) {
final Segment[] ss = this.segments; // 当前的segments数组
long u = (k << SSHIFT) + SBASE; // raw offset // 计算原始偏移量,在segments数组的位置
Segment seg;
if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u)) == null) { // 判断没有被初始化
Segment proto = ss[0]; // use segment 0 as prototype // 获取第一个segment ss[0]
// 这就是为什么要在初始化化map时要初始化一个segment,需要用cap和loadFactoe 为模板
int cap = proto.table.length; // 容量
float lf = proto.loadFactor; // 负载因子
int threshold = (int)(cap * lf); // 阈值
// 初始化ss[k] 内部的tab数组 // recheck
HashEntry[] tab = (HashEntry[])new HashEntry[cap];
// 再次检查这个ss[k] 有没有被初始化
if ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
// 创建一个Segment
Segment s = new Segment(lf, threshold, tab);
// 这里用自旋CAS来保证把segments数组的u位置设置为s
// 万一有多线程执行到这一步,只有一个成功,break
// getObjectVolatile 保证了读的可见性,所以一旦有一个线程初始化了,那么就结束自旋
while ((seg = (Segment)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
scanAndLockForPut自旋获取锁方法
具体流程看代码注释,解释下这个方法的优化(看的某位大佬的博客)
(1) 我们在put方法获取锁失败,才会进入这个方法,这个方法采用自旋获取锁,直到成功才返回,但是使用了自旋次数的限制,这么做的好处是什么了,就是竞争太激烈的话,这个线程可能一直获取不到锁,自旋也是消耗cpu性能的,所以当达到自旋次数时,就阻塞当前线程,直到有线程释放了锁,通知这些线程.在等待过程中是不消耗cpu的.
(2) 当我们进入这个方法时,说明获取锁失败,那么可别是别的线程在对这个segment进行修改操作,所以说如果别的线程在操作之后,我们自己的工作内存中的数据可能已经不是最新的了,这个时候我们使用具有volatile语义的方法重新读了数据,在自旋过程中遍历这些数据,把最新的数据缓存在工作内存中,当前线程再次获取锁时,我们的数据是最新的,就不用重新去住内存中获取,这样在自旋获取的锁的过程中就预热了这些数据,在获取锁之后的执行中就提升了效率.
private HashEntry scanAndLockForPut(K key, int hash, V value) {
HashEntry first = entryForHash(this, hash); // 根据hash获取头结点
HashEntry e = first;
HashEntry node = null;
int retries = -1; // 是为了找到对应hash桶,遍历链表时找到就停止
while (!tryLock()) { // 尝试获取锁,成功就返回,失败就开始自旋
HashEntry f; // to recheck first below
if (retries < 0) {
if (e == null) { // 结束遍历节点
if (node == null) // 创造新的节点
node = new HashEntry(hash, key, value, null);
retries = 0; // 结束遍历
}
else if (key.equals(e.key)) // 找到节点 停止遍历
retries = 0;
else
e = e.next; // 下一个节点 直到为null
}
else if (++retries > MAX_SCAN_RETRIES) { // 达到自旋的最大次数
lock(); // 进入加锁方法,失败进入队列,阻塞当前线程
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // 头结点变化,需要重新遍历,说明有新的节点加入或者移除
retries = -1;
}
}
return node;
}
rehash扩容方法
解释下节点位置变化这一块的处理,如下图所示.
/**
*扩容方法
*/
private void rehash(HashEntry node) {
// 旧的table
HashEntry[] oldTable = table;
// 旧的table的长度
int oldCapacity = oldTable.length;
// 扩容原来capacity的一倍
int newCapacity = oldCapacity << 1;
// 新的阈值
threshold = (int)(newCapacity * loadFactor);
// 新的table
HashEntry[] newTable =
(HashEntry[]) new HashEntry[newCapacity];
// 新的掩码
int sizeMask = newCapacity - 1;
// 遍历旧的table
for (int i = 0; i < oldCapacity ; i++) {
// table中的每一个链表元素
HashEntry e = oldTable[i];
if (e != null) { // e不等于null
HashEntry next = e.next; // 下一个元素
int idx = e.hash & sizeMask; // 重新计算位置,计算在新的table的位置
if (next == null) // Single node on list 证明只有一个元素
newTable[idx] = e; // 把当前的e设置给新的table
else { // Reuse consecutive sequence at same slot
HashEntry lastRun = e; // 当前e
int lastIdx = idx; // 在新table的位置
for (HashEntry last = next;
last != null;
last = last.next) { // 遍历链表
int k = last.hash & sizeMask; // 确定在新table的位置
if (k != lastIdx) { // 头结点和头结点的next元素的节点发生了变化
lastIdx = k; // 记录变化位置
lastRun = last; // 记录变化节点
}
}
// 以下把链表设置到新table分为两种情况
// (1) lastRun 和 lastIdx 没有发生变化,也就是整个链表的每个元素位置和一样,都没有发生变化
// (2) lastRun 和 lastIdx 发生了变化,记录变化位置和变化节点,然后把变化的这个节点设置到新table
// ,但是整个链表的位置只有变化节点和它后面关联的节点是对的
// 下面的这个遍历就是处理这个问题,遍历当前头节点e,找出不等于变化节点(lastRun)的节点重新处理
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry n = newTable[k];
newTable[k] = new HashEntry(h, p.key, v, n);
}
}
}
}
// 处理扩容时那个添加的节点
// 计算位置
int nodeIndex = node.hash & sizeMask; // add the new node
// 设置next节点,此时已经扩容完成,要从新table里面去当前位置的头结点为next节点
node.setNext(newTable[nodeIndex]);
// 设置位置
newTable[nodeIndex] = node;
// 新table替换旧的table
table = newTable;
}
get没有加锁,效率高
注意:get方法使用了getObjectVolatile方法读取segment和hashentry,保证是最新的,具有锁的语义,可见性
分析:为什么get不加锁可以保证线程安全
(1) 首先获取value,我们要先定位到segment,使用了UNSAFE的getObjectVolatile具有读的volatile语义,也就表示在多线程情况下,我们依旧能获取最新的segment.
(2) 获取hashentry[],由于table是每个segment内部的成员变量,使用volatile修饰的,所以我们也能获取最新的table.
(3) 然后我们获取具体的hashentry,也时使用了UNSAFE的getObjectVolatile具有读的volatile语义,然后遍历查找返回.
(4) 总结我们发现怎个get过程中使用了大量的volatile关键字,其实就是保证了可见性(加锁也可以,但是降低了性能),get只是读取操作,所以我们只需要保证读取的是最新的数据即可.
/**
* get 方法
*/
public V get(Object key) {
Segment s; // manually integrate access methods to reduce overhead
HashEntry[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 获取segment的位置
// getObjectVolatile getObjectVolatile语义读取最新的segment,获取table
if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// getObjectVolatile getObjectVolatile语义读取最新的hashEntry,并遍历
for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
// 找到相同的key 返回
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
size尝试3次不加锁获取sum,如果发生变化就全部加锁,size和containsValue方法的思想也是基本类似.
执行流程
(1) 第一次,retries++=0,不满足全部加锁条件,遍历所有的segment,sum就是所有segment的容量,last等于0,第一次不相等,last=sum.
(2) 第二次,retries++=1,不满足加锁条件,计算所有的segment,sum就是所有的segment的容量,last是上一次的sum,相等结束循环,不相等下次循环.
(3) 第三次,retries++=2,先运算后赋值,所以此时还是不满足加锁条件和上面一样统计sum,判断这一次的sum和last(上一次的sum)是否相等,相等结束,不相等,下一次循环.
(4) 第四次,retries++=2,满足加锁条件,给segment全部加锁,这样所有线程就没有办法进行修改操作,统计每个segment的数量求和,然后返回size.(ps:全部加锁提高了size的准确率,但是降低了吞吐量,统计size的过程中如果其它线程进行修改操作这些线程全部自旋或者阻塞).
/**
* size
* @return
*/
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[] segments = this.segments;
int size;
boolean overflow; // 为true表示size溢出32位
long sum; // modCounts的总和
long last = 0L; // previous sum
int retries = -1; // 第一次不计算次数,所以会重试三次
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) { // 重试次数达到3次 对所有segment加锁
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 seg = segmentAt(segments, j);
if (seg != null) { // seg不等于空
sum += seg.modCount; // 不变化和size一样
int c = seg.count; // seg 的size
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;
}
removereplace和remove都是用了scanAndLock这个方法
解释下scanAndLock这个方法,和put方法的scanAndLockForPut方法思想类似,都采用了同样的优化手段.
/**
* 删除方法
*/
final V remove(Object key, int hash, Object value) {
if (!tryLock()) // 获取锁
scanAndLock(key, hash); // 自旋获取锁
V oldValue = null;
try {
HashEntry[] tab = table; // 当前table
int index = (tab.length - 1) & hash; // 获取位置
HashEntry e = entryAt(tab, index);// 找到元素
HashEntry pred = null;
while (e != null) {
K k;
HashEntry next = e.next; // 下一个元素
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) { // e的key=传入的key
V v = e.value; // 获取value
if (value == null || value == v || value.equals(v)) { // 如果value相等
if (pred == null) // 说明是头结点,让next节点为头结点即可
setEntryAt(tab, index, next);
else // 说明不是头结点,就把当前节点e的上一个节点pred的next节点设置为当前e的next节点
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
isEmpty其实也和size的思想类似,不过这个始终没有加锁,提高了性能
执行流程
(1) 第一次遍历就干了一件事,确定map的每个segment是否为0,其中任何一个segment的count不为0,就返回,都为0,就累加modCount为sum.
(2) 判断sum是否为0,不为0,就代表了map之前是有数据的,被remove和clean了,modCount指的是操作次数,再次确定map的每个segment是否为0,其中任何一个segment的count不为0,就返回,并且累减sum,最后判断sum是否为0,为0就代表没有任何变化,不为0,就代表在第一次统计过程中有线程又添加了元素,所以返回false.但是如果在第二次统计时又发生了变化了,所以这个不是那么的准确,但是不加锁提高了性能,也是一个可以接受的方案.
```public boolean isEmpty() {
long sum = 0L;
final Segment[] segments = this.segments;
for (int j = 0; j < segments.length; ++j) {
Segment seg = segmentAt(segments, j);
if (seg != null) {
if (seg.count != 0)
return false; // 某一个不为null,立即返回
sum += seg.modCount;
}
}
// 上面执行完 说明不为空,并且过程可能发生了变化
// 发生变化
if (sum != 0L) { // recheck unless no modifications
for (int j = 0; j < segments.length; ++j) {
Segment seg = segmentAt(segments, j);
if (seg != null) {
if (seg.count != 0)
return false;
sum -= seg.modCount;
}
}
if (sum != 0L) // 变化值没有为0,说明不为空
return false;
}
// 没有发生变化
return true;
}
1.7 ConcurrentHashMap 使用了分段锁的思想提高了并发的的访问量,就是使用很多把锁,每一个segment代表了一把锁,每一段只能有一个线程获取锁;但是segment的数量初始化了,就不能修改,所以这也代表了并发的不能修改,这也是1.7的一个局限性.从get方法可以看出使用了UNSAFE的一些方法和volatile关键字来代替锁,提高了并发性.在size和containsValue这些方法提供一种尝试思想,先不加锁尝试统计,如果其中没有变化就返回,有变化接着尝试,达到尝试次数再加锁,这样也避免了立即加锁对并发的影响.