1 概述
HashMap 是 Java Collection Framework 的重要成员,也是Map族(如下图所示)中我们最为常用的一种。不过遗憾的是,HashMap不是线程安全的。也就是说,在多线程环境下,操作HashMap会导致各种各样的线程安全问题,比如在HashMap扩容重哈希时出现的死循环问题,脏读问题等。HashMap的这一缺点往往会造成诸多不便,虽然在并发场景下HashTable和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。庆幸的是,JDK为我们解决了这个问题,它为HashMap提供了一个线程安全的高效版本 —— ConcurrentHashMap。
在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。
2 实现原理
1 如何实现高效线程安全
ConcurrentHashMap 实际上是由多个 Segment(分段)组成, Segment 的内部结构与 HashMap 一样,都是由哈希表构成。因此也可以理解为由多个 “HashMap” 构成。由于Segment类继承于ReentrantLock类,从而使得Segment对象能充当锁的角色,这样每个 Segment对象就可以守护其小哈希表中若干个桶读写线程安全,其中每个桶是由若干个HashEntry 对象链接起来的链表。<font color=red >通过使用段(Segment)将ConcurrentHashMap划分为不同的部分,ConcurrentHashMap就可以使用不同的锁来控制对哈希表的不同部分的修改,从而允许多个修改操作并发进行, 这正是ConcurrentHashMap锁分段技术的核心内涵。</font>
image2 如何实现读不加锁
ConcurrentHashMap每一个节点对象HashEntry,key,hash和next属性都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。
static final class HashEntry<K,V> {
final K key; // 声明 key 为 final 的
final int hash; // 声明 hash 值为 final 的
volatile V value; // 声明 value 被volatile所修饰
final HashEntry<K,V> next; // 声明 next 为 final 的
next声明为final
next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,因此所有的节点的修改只能从头部开始。
-
对于put操作,可以一律添加到Hash链的头部。
-
是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的下一个结点(这在谈到ConcurrentHashMap的删除操作时还会详述)。
这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变,话句话说当我们查询Map中数据进行遍历链表时不会受到写线程的影响。
value声明为volatile
- 由于value域被volatile修饰,所以其可以确保被读线程读到最新的值
这个特性可以保证:在访问某个节点时,这个节点之后被其他线程改变当前线程能看到。
不允许用null作为键和值
由于在ConcurrentHashMap中不允许用null作为键和值,所以当读线程读到某个HashEntry的value为null时,便知道产生了冲突 —— 发生了重排序现象,此时便会加锁重新读入这个value值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。
总的来说,ConcurrentHashMap读操作不需要加锁的奥秘在于以下三点:
-
<font color=red >用HashEntery对象的不变性来降低读操作对加锁的需求;</font>
-
<font color=red >用Volatile变量协调读写线程间的内存可见性;</font>
-
<font color=red >若读时发生指令重排序现象,则加锁重读;</font>
3 ConcurrentHashMap 源码解析
3.1 ConcurrentHashMap 类结构
image- ConcurrentHashMap继承于AbstractMap抽象类。
- Segment是ConcurrentHashMap中的内部类,它就是ConcurrentHashMap中的“锁分段”对应的存储结构。ConcurrentHashMap与Segment是组合关系,1个ConcurrentHashMap对象包含若干个Segment对象。在代码中,这表现为ConcurrentHashMap类中存在“Segment数组”成员。
- Segment类继承于ReentrantLock类,所以Segment本质上是一个可重入的互斥锁
- HashEntry也是ConcurrentHashMap的内部类,是单向链表节点,存储着key-value键值对。Segment与HashEntry是组合关系,Segment类中存在“HashEntry数组”成员,“HashEntry数组”中的每个HashEntry就是一个单向链表。
3.2 核心属性
/**
* 用于定位段,大小等于segments数组的大小减 1,是不可变的
*/
final int segmentMask;
/**
*// 用于定位段,大小等于32(hash值的位数)减去对segments的大小取以2为底的对数值,是不可变的
*/
final int segmentShift;
/**
* ConcurrentHashMap的底层结构segment数组
*/
final Segment<K,V>[] segments;
3.3 Segment的定义
Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护它的成员对象 table 中包含的若干个桶。table 是一个由 HashEntry 对象组成的链表数组,table 数组的每一个数组成员就是一个桶。
段(Segment) 定义
static final class Segment<K,V> extends ReentrantLock implements Serializable {
/**
* 哈希表
*/
transient volatile HashEntry<K,V>[] table;
/**
* Segment中元素的数量,可见的
*/
transient int count;
/**
* 对count的大小造成影响的操作的次数
*/
transient int modCount;
/**
* 阈值,段中元素的数量超过这个值就会对Segment进行扩容
*/
transient int threshold;
/**
* 段的负载因子
*/
final float loadFactor;
}
- 在Segment类中,count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组包含的 HashEntry 对象的个数,也就是 Segment 中包含的 HashEntry 对象的总数。之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是对 ConcurrentHashMap 并发性的考虑:因为这样当需要更新计数器时,不用锁定整个ConcurrentHashMap。每次对段进行结构上的改变,如在段中进行增加/删除节点(修改节点的值不算结构上的改变),都要更新count的值,此外,在JDK的实现中每次读取操作开始都要先读取count的值。特别需要注意的是,count是volatile的,这使得对count的任何更新对其它线程都是立即可见的。
- modCount用于统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变,这一点具体在谈到跨段操作时会详述。
- threashold用来表示段需要进行重哈希的阈值。
- loadFactor表示段的负载因子,其值等同于ConcurrentHashMap的负载因子的值。
- table是一个拉链表实现的哈希表,而且也是volatile的,这使得对table的任何更新对其它线程也都是立即可见的
3.4 HashEntry 定义
HashEntry用来封装具体的键值对与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value和next。不同的是,在HashEntry类中,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。
next声明为final
next域被声明为final本身就意味着我们不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,因此所有的节点的修改只能从头部开始。
-
对于put操作,可以一律添加到Hash链的头部。
-
是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制(重新new)一遍,最后一个节点指向要删除结点的下一个结点(这在谈到ConcurrentHashMap的删除操作时还会详述)。
value声明为volatile
- 由于value域被volatile修饰,所以其可以确保被读线程读到最新的值
static final class HashEntry<K,V> {
final K key; // 声明 key 为 final 的
final int hash; // 声明 hash 值为 final 的
volatile V value; // 声明 value 被volatile所修饰
final HashEntry<K,V> next; // 声明 next 为 final 的
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
@SuppressWarnings("unchecked")
static final <K,V> HashEntry<K,V>[] newArray(int i) {
return new HashEntry[i];
}
}
与HashMap类似,在ConcurrentHashMap中,如果在散列时发生碰撞,也会将碰撞的 HashEntry 对象链成一个链表。由于HashEntry的next域是final的,所以新节点只能在链表的表头处插入。下图是在一个空桶中依次插入 A,B,C 三个 HashEntry 对象后的结构图(由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反):
image3.5 构造函数
该构造函数意在构造一个具有指定容量、指定负载因子和并发级别的空ConcurrentHashMap
-
initialCapacity(总容量):用来表示ConcurrentHashMap初始总容量,等于所有段哈希表容量总和。用于计算每个哈希表初始化容量 initialCapacity/段数量
-
loadFactor(装载因子):用来表示每个段哈希表装载因子,用来计算扩容的阀值。
-
concurrencyLevel(并发级别):用来决定段数组大小,通常大小为大于concurrencyLevel最小的2^n
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
/** 参数校验 **/
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
/** 并发级别不能大于MAX_SEGMENTS(16) **/
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
/** sshift 表示数学公式lg(ssize)的值 **/
int sshift = 0;
/** ssize表示segments数组的大小(2的幂次方) **/
int ssize = 1;
/** 计算ssize,ssize **/
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
/** 用于定位key属于哪个段 hash(key) >>> segmentShift) & segmentMask=(ssize(2^n)-1) **/
this.segmentShift = 32 - sshift;
/** 用于定位key属于哪个段 hash(key) >>> segmentShift) & segmentMask=(ssize(2^n)-1) **/
this.segmentMask = ssize - 1;
/** 初始化总数 **/
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
/** 计算段的哈希表初始容量cap **/
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
/** 实例化模板segment **/
Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]);
/** 实力化Segment[] ss **/
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
/** 将模板segment,放入数组第一个下标处Segment[0] **/
UNSAFE.putOrderedObject(ss, SBASE, s0);
this.segments = ss;
}
3.6 并发插入
/**
* 将指定的键映射到此表中的指定值。键和值都不能为空。
*/
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment<K,V> s;
/** 不允许为null的value **/
if (value == null)
throw new NullPointerException();
/** 计算key hash值 **/
int hash = hash(key);
/**
* 计算当前key所属于Segment数组哪个下标的Segment对象
* **/
int j = (hash >>> segmentShift) & segmentMask;
/**
* 使用UNSAFE.getObjectVolatile获取segments数组对象指定下标的Segment,如果不存在
* 则调用ensureSegment创建Segment
**/
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
/** 返回Segment[]数组指定下标Segment对象,如果不存在则创建Segment **/
s = ensureSegment(j);
/** 段中插入 **/
return s.put(key, hash, value, false);
}
3.6.1 创建Segment
ensureSegment用来返回Segment[]数组指定下标Segment对象,如果不存在则创建Segment,该方法并没有使用锁,使用自旋+CAS保证当segments数组下标k不存在元素时,设置为新建Segment对象如果其他线程设置segments数组下标对象Segment则直接返回
/**
* 返回Segment[]数组指定下标Segment对象,如果不存在则创建Segment
* 该方法并没有使用锁,使用自旋+CAS保证当segments数组下标k不存在元素时,设置为新建Segment对象
* 如果其他线程设置segments数组下标对象Segment则直接返回。
*/
@SuppressWarnings("unchecked")
private Segment<K,V> ensureSegment(int k) {
/** 获取保存segments数组 **/
final Segment<K,V>[] ss = this.segments;
/** 计算segments数组中指定下标k在内存中偏移量 **/
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
/** 使用UNSAFE.getObjectVolatile获取segments数组对象指定下标的Segment **/
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
/** 获取第一个Segment,通常数组中一个Segment作为创建其他Segment模板 **/
Segment<K,V> proto = ss[0];
/** 获取模板Segment哈希表容量 **/
int cap = proto.table.length;
/** 获取模板Segment 哈希表装载因子**/
float lf = proto.loadFactor;
/** 计算哈希表扩容阀值 **/
int threshold = (int)(cap * lf);
/** 实例化哈希表 **/
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
/** double check 使用UNSAFE.getObjectVolatile获取segments数组对象指定下标的Segment **/
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
/** 创建Segment **/
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
/** 使用自旋+CAS保证当segments数组下标k不存在元素时,设置为新建Segment对象 s **/
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
3.6.2 Segment中插入
ConcurrentHashMap对Segment的put操作是加锁完成的。Segment是ReentrantLock的子类,因此Segment本身就是一种可重入的Lock,所以我们可以直接调用其继承而来的lock()方法和unlock()方法对代码进行上锁/解锁。和HashMap首先找到key对应哈希表中桶(链表),遍历链表中节点是否存在插入节点,如果存在则覆盖value,如果不存在则添加到链表头部。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
/** 获取锁,失败调用scanAndLockForPut在自旋获取锁(直到成功) **/
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;;) {
/**
* 查找链表节点key&hash相同HashEntry(用来表示插入的key-value以存在于Map中)如果找到则覆盖Entry.value,
*/
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;
}
/**
* 如果链表中不存在则新建HashEntry,添加到链表的头部
* */
else {
/** 如果在scanAndLockForPut已经实例化则不在实例化,只用设置next **/
if (node != null)
node.setNext(first);
/** 实例化HashEntry,将next指向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
/** 新建HashEntry设置到哈希表指定下标index **/
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
/** 释放锁 **/
unlock();
}
return oldValue;
}
3.6.3 自旋获取锁
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
/** 通过hash值获取哈希表中HashEntry对象 **/
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;
/**
表示第一次进入初始值或哈希表被其他线程改变重置值,通过hash值获取哈希表中HashEntry对象,判断哈希表中桶中是否存在HashEntry对象,
* 如果存在,以获取HashEntry对象作为链表头节点开始向后遍历,找到key相同的HashEntry对象。
* 如果不存在,创建一个新的HashEntry对象
* 最后设置retries=0
*/
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;
}
/**
* ++retries > MAX_SCAN_RETRIES 当自旋次数超过限制,这就表明竞争太激烈的话,这个线程可能一直获取不到锁,自旋也是消耗cpu性能的,
* 所以当达到自旋次数时,就阻塞当前线程,直到有线程释放了锁。
*/
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
/** 哈希表被其他线程改变重置retries=-1 **/
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
3.7 Segment扩容
在ConcurrentHashMap中使用put操作插入Key/Value对之前,首先会检查本次插入会不会导致Segment中节点数量超过阈值threshold,如果会,那么就先对Segment进行扩容和重哈希操作。特别需要注意的是,ConcurrentHashMap的重哈希实际上是对ConcurrentHashMap的某个段的重哈希。
核心流程
Segment扩容大体逻辑是遍历哈希表每个一个链表。将链表中每个节点复制到新的桶中(链接指针next是final的)。但事实上JDK对其做了一定的优化。
优化
因为在理论上原桶里的HashEntry链可能存在一条子链,这条子链上的节点都会被重哈希到同一个新的桶中,这样我们只要拿到该子链的头结点就可以直接把该子链放到新的桶中,从而避免了一些节点不必要的创建,提升了一定的效率。因此,JDK为了提高效率,它会首先去查找这样的一个子链,而且这个子链的尾节点必须与原hash链的尾节点是同一个,那么就只需要把这个子链的头结点放到新的桶中,其后面跟的一串子节点自然也就连接上了。对于这个子链头结点之前的结点,JDK会挨个遍历并把它们复制到新桶的链头(只能在表头插入元素)中。
private void rehash(HashEntry<K,V> node) {
/** 获取原始哈希表 **/
HashEntry<K,V>[] oldTable = table;
/** 获取原始哈希表容量 **/
int oldCapacity = oldTable.length;
/** 扩容原来capacity的一倍 **/
int newCapacity = oldCapacity << 1;
/** 计算扩容后阀值 **/
threshold = (int)(newCapacity * loadFactor);
/** 新建扩容后创建哈希表**/
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
/** 新的掩码,用于计算hash对应哈希表下标 **/
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;
/** 链表只有一节点**/
if (next == null)
/** 将该节点设置新链表中 **/
newTable[idx] = e;
/** 链表存在多个节点**/
else {
/** 遍历链表中的节点,找到链表中最后一个当前链表头节点不属于一个哈希表下标的子链表**/
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
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;
/** 对链表前子链之前的结点,JDK会挨个遍历并把它们复制到新桶中 **/
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);
}
}
}
}
/** 将新节点添加到哈希表中 **/
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
3.8 读取
首先获取指定哈希值对应的段Segment,在Segment内根据hash值找到哈希表对应的链表,然后遍历这个链表找到要查询节点
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
/** 计算key hash值 **/
int h = hash(key);
/** 获取指定hash值对应的段Segment在数组下标 **/
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
/** 获取对应segment对象 **/
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
/** 在Segment内根据hash值找到哈希表对应的链表,遍历链表 表找到要查询节点**/
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
/** 返回链表的值 **/
return e.value;
}
}
return null;
}
3.9 删除
Segment的remove操作和前面提到的get操作类似,首先获取指定哈希值对应的段Segment,在Segment内根据hash值找到哈希表对应的链表,然后遍历这个链表找到要删除的节点,对于不是需要删除的节点重新添加到链表头部,最后把待删除节点之后的所有节点原样保留在新链表中添加到链表的尾部
imagepublic V remove(Object key) {
/** 计算key hash值 **/
int hash = hash(key);
/** 获取指定hash值对应的段Segment **/
Segment<K,V> s = segmentForHash(hash);
/** 从指定Segment 删除key **/
return s == null ? null : s.remove(key, hash, null);
}
final V remove(Object key, int hash, Object value) {
/** 尝试获取锁,失败调用scanAndLock阻塞当前线程,直到有线程释放锁 **/
if (!tryLock()){
scanAndLock(key, hash);
}
V oldValue = null;
try {
/** 获取哈希表 **/
HashEntry<K,V>[] tab = table;
/** 计算hash值对应的哈希表下标**/
int index = (tab.length - 1) & hash;
/** 获取下标对应HashEntry对象 **/
HashEntry<K,V> e = entryAt(tab, index);
/** 链表中删除节点前置节点引用 **/
HashEntry<K,V> pred = null;
/** 判断哈希表index下标桶是否存在HashEntry对象,如果存在,以HashEntry对象开始,作为链表头部开始向后遍历 **/
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
/**
* 遍历链表中HashEntry对象,
* 如果原始查找的元素为x,链表原始a->b->x->c->d
* 如果找到key&hash不相同重新设置链表的头部,并将最后节点被pro指向(a)
* b->a
* 如果找到key&hash相同,设置到pro之后
* b->a->x->c->d
**/
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;
}
网友评论