ConcurrentHashMap
实例化方法
在HashMap中, 主要有2个参数 一个 是 初始容量大小 , 一个是 负载因子
但是在ConcurrentHashMap中 , 还出现了另外一个 重要的 参数, 叫做 并发等级
在所有的构造方法中, 主要有2个构造方法, 一个是 具有3参数的 正常构造器, 一个是基于 Map的构造器 ,
基于map的构造器, 我们可以先不用看 . .
在正常的构造器中, 构造主要有 3个步骤
- 判断参数是否正常
- 参数处理
- 实例化Segment数组
第一个步骤 : 判断参数是否正常
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
都不允许是负数. 使用 !(localFacory>0) 是因为 负载因子 同时不能为0
第二个步骤 : 处理参数
这里可以细分为 处理并发等级 和 处理初始容量 2个小步骤
// 2.1 处理并发等级
if (concurrencyLevel > MAX_SEGMENTS) //MAX_SEGMENTS = 2^16 最大的Segment数量.
concurrencyLevel = MAX_SEGMENTS; //可以把并发等级看成是Segment数量
/*
这边 可以做一些简单的猜测... 之前对HashMap的观察
一个key hash 出来之后 肯定是一个int的数 , 这个数 有32位
sshift 在这个并发等级下, 需要多少位,, 也就是说 用多少位来表示 segment数量
ssize 目前来看就是 并发数量... 也就是Segment 实际的数量
ssize >= concurrencyLevel 且是一个 2次方数
*/
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
/**
有了上面的猜测.. 看一下这里. 这里保留了2个参数
一个是 segmentShift : 32 - sshift , 也就说 除了用来区分Segment..剩下的位数..
这个位数 可能是 用来做 hash 分到Segment 之后再分配到 table里
*/
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1; // ssize 是一个 2次方数..所以 - 1 会造成低位全部都是 1
//根据翻译..Segment 掩码.. 掩码就是 掩盖掉 不需要的位数...
// 2.2 处理初始容量
if (initialCapacity > MAXIMUM_CAPACITY) //MAXIMUM_CAPACITY = 2^30 这个和HashMap是一样的
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize; //ssize 当时是说 比并发等级大 且是 一个2次方数
//但是注意..这里的初始容量并没用和hashmap一样做处理..没有形成一个2的次方数
//但是因为底层是一个Segment数组..所以最后是使用ssize为基准的. 最后还是一个
//ssize当时说可以看成是一个 Segment的数量... 那么 整体容量 / Sement数量... 这不就得到了每个Sement大概需要多少个 table 吗
if (c * ssize < initialCapacity) // 因为 / 可能出现除不尽的现象.. 这就导致 c* ssize 可能小于初始值.. 这里要再加回来.. 最后结果 实际 c * ssize >= 初始容量
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY; //MIN_SEGMENT_TABLE_CAPACITY=2 Segment中的table数组,最小为 2
/**
这里就出现了一个奇怪的现象.... 默认的容量是16 , 默认的并发等级是16 但是每个Segment最小的table数组长度又是2 ,
这就导致了 虽然看上去 初始化容量是16 .. 但是底层实际是有 32 个长度的 hashEntry数组
*/
while (cap < c) //cap 就是 每个Segment中的 table数 cap最后不得小于c 且是一个2次方数
cap <<= 1;
第三个步骤 : 对Segment数组进行实例化
/**
这里发现了个东西... 它首先去实例化一个叫s0的东西..用来保存了 负载因子..threshold阈值 , 以及一个需要的HashEntry的数组....ss 是最后需要的 Segment数组.. 最后使用了一个 UNSAFE的方法(UNSAFE的方法的都是native方法- -不知道干嘛)...这因为命名叫s0 .. 又是数组.. 这里猜测它就是把 s0放进 ss数组的下标为0的位置
ps : 那为啥不先 ss = new Segment[ssize]; ss[0] = new Segmeng.....一定要炫技的吗?还是说为了提升速度
还是说使用UNSAFE快一些
这里纯属猜测..继续深入下去 才能知道 什么情况..
原来的hashMap是允许 key 作为null 的 而且存放在 0的位置...
但是Concurrent是不能为null的, value 也不能为null
**/
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]; //ssize是实际的Segment数量..这里也能看出来
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
Map为Put而生
在ConcurrentHashMap中 使用的底层存储 单元 是一个名叫 Segment的东西
static final class Segment<K,V> extends ReentrantLock implements Serializable
继承了锁的重入锁的能力…
在每个Segment中存在实际的 存储单元 HashEntry 数组
transient volatile HashEntry<K,V>[] table
对于一个Map 最重要的 方法 之一 就是 put
public V put(K key, V value) {
Segment<K,V> s; //寻找出要存储的Segment 区间... 还是靠key的hash 来分区..
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j); //因为之前虽然new了一个数组..但是里面的元素都是空的.. 也就是会出现懒加载的现象.. 之后再实例化. 这个ensureSegment方法大概就是了.
//不过底层使用的是UNSAFE 来直接获得Segment....
return s.put(key, hash, value, false); //调用 Segment的Put方法
}
/****
if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);
**/
public V putIfAbsent(K key, V value) { //和上面唯一的区别 就是 最后一个参数为true
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
(segments, (j << SSHIFT) + SBASE)) == null)
s = ensureSegment(j);
return s.put(key, hash, value, true);
}
这样之后…就从map的 put方法 转移到了 Segment的 put方法
在这之前先不要着急… 让我们来分析一下 hash() 方法 以及 这个 j 是如何运算的
private int hash(Object k) {
int h = hashSeed; //这个hashSeed 是和 虚拟机的参数有关.. 在不配置的情况下 默认为0 .我们就是用默认的分析吧
if ((0 != h) && (k instanceof String)) { //如果配置了参数..且是String类型的, 就使用hash32
return sun.misc.Hashing.stringHash32((String) k);
}
// k.hashCode ... 这里可以看出..虽然 key 没有明确判断不能为null ..但是 一旦是null
//这里就会空指针了...
h ^= k.hashCode(); //hashmap一样的操作..
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
//这里发生变化了! 看上去一顿乱移...emm 应该是某个hash算法... 反正就是为了增加散列度...
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
return h ^ (h >>> 16);
}
关键的来了… 让我们来分析一下这个 j 是个啥运算
/*
在上文中.. 我们知道了 segmentShift 是 除掉了Segment位数之后的位置 , segmentMask 是个低位全部都是1的数
这样的运算 首先让 hash 右移 (32-segment数量)...
这里也就是说 高segment位 是用来区分segment的 & 一个掩码
做几个例子 更加直观 . 加入就以默认的来算 segmentShift = 28 (2^4 = 16)(32 - 4 = 28)
segmentMask = 0000...0000 1111
最后得到的结果 也就是取 高位.
ps: 这里为什么要使用无符号的移位运算呢 .. ssize = 2^sshift
segmentShift = 32 - sshift
segmentMask = ssize - 1
也就是说 如果 hash >>> segmentShift 不就是最后的j了吗- -
为啥还要再& 一下 掩码...emm
**/
int j = (hash >>> segmentShift) & segmentMask;
也就是说 这个 j 可以表示的是 放入到哪个segment中去… 之后UNSAFE 就是使用j 来找到对应的 Segment…
有了j之后… 让我们看看 ensureSegment(j) 如何实例化这个 Segment 对象的
ConcureentHashMap , 都知道 是个并发容器… 也就是意味着是支持多个线程同时访问的…
所以先要判断 这个方法会不会被多个线程同时进入 而且 k相同? 答案是肯定的…
但是我们并没有发现任何的锁…那么如何保证线程安全呢 ? CAS ( 看这个方法是如何使用 CAS 进行线程安全的)
private Segment<K,V> ensureSegment(int k) {
//这里是都是简单流程.. 通过k 获得 u(应该是某个偏移量) 通过u再获得对应的segment
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]; // s0出现了! 也就是说 s0可能是作为样板存在的
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); //new 还是new出来了
//也就是说 多线程走到这里的话.. 空间还是损耗掉了
//使用一个自旋的CAS 来保证线程安全
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
//如果不为true 就是说明有线程已经创造好了 , 当前线程再次进入while判断的时候就会false正常结束
//如果为true 说明当前线程是第一个完成的, 正常break 跳出
//造成的线程安全
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
上面这些都捋完了… 终于来到了 Segment的 put方法了
疯狂粘贴源码…篇幅好像过长… 所以这里尝试截断的方式来…
首先put方法也会同时被很多很多的线程同时访问… 之前说了Segment是继承了锁的.意味着可以随时随地的使用lock来保证线程安全 所以 put方法 可以大致 描述成
- 尝试一下能不能拿到锁, 拿到锁 就进入正常的 put方法, 如果拿不到锁, 就进入一个准备阶段
scanAndLockForPut
方法
HashEntry<K,V> node = tryLock() ? null : //开篇先使用一下tryLock 看看能不能得到锁
scanAndLockForPut(key, hash, value);
-
如果得不到锁 就会进入 scanAndLockForPut 准备 出一个新的 node … ps: 虽然感觉没必要… 听我慢慢解释
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 //假设在一直失败的情况下.. retries = -1 if (retries < 0) { if (e == null) { if (node == null) // speculatively create node node = new HashEntry<K,V>(hash, key, value, null); retries = 0; //如果 没有找到节点.. 意味着是新的.. 先构造出一个节点. retries设置为0 } else if (key.equals(e.key)) //如果 找到了一个节点.. 这个节点是存在的 retries = 0; //也设置为0 重试 else e = e.next; //否则接着寻找 } else if (++retries > MAX_SCAN_RETRIES) { // 思考~ 如果有线程lock住了... 会不会死锁. lock(); //如果超过了重试次数.. 那就直接 使用lock阻塞. 直到强到锁位置 break; } else if ((retries & 1) == 0 && //retries & 1 也就是说 每2次 就去看一眼自己的 槽有没有发生改变.. (f = entryForHash(this, hash)) != first) { //如果发生改变... 那么就重新寻找 e = first = f; // re-traverse if entry changed retries = -1; } } /** * 这里会不会出现 上面分支 最后一条 判断的时候 还没有table 还没有改变 * 判断结束之后 刚发生变化 * 然后 tryLock的时候 获取了锁...这样导致返回的 node 虽然不是null 但是 实际 key 是存在的- -? * */ //node有值 说明是新的点 //node 没有值 说明是有的..只需要修改 return node; }
从源码中 我们可以看出来… retries & 1 也就是以为这 每2次 trylock失败 就会查询一遍 当前插入的槽 是否已经出现了改变, 如果出现改变, 那就重新准备 , 但是并非是一直在轮询… 也就是说node 其实并不相信 , node 为 null 可能是有重复值… node有值 也可能在trylock成功之前被其他线程修改了… 那么如何处理这样的情况呢
-
进入正常的put ( 隐藏了方法体 ) (这一段代码 是lock 之后的, 也就是说 临界区)
V oldValue; //获得了锁 try { HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; //使用和hashmap相同的方式获得index //回顾一下.. hashtable 的index计算方式是 先去掉 hash值的符号位 //然后 在 % tab的长度 HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;;) { //依然需要遍历一遍.. 来确定 node 是否有用... //ps: 所以说感觉- -那个scanAndLockForPut去准备一个node没啥必要- -...就正常循环着等待也行(可能是能快一点是一点吧) if (e != null) { //下面是正常的寻找节点的方式 K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { //如果之前有的就替换 oldValue = e.value; if (!onlyIfAbsent) {//区分put 和 putIfAbsent (put 是会替代的 , 但是putIfAbsent只插入不存在的值...存在的话 就返回 执行结束) e.value = value; ++modCount; } break; } e = e.next; } else { //不存在这个点 if (node != null) //如果是 scan 那个方法进来的话,,,就有可能提前准备好了 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); //使用UNSAFE方法 塞进去... ++modCount; //结构修改次数+1 count = c; oldValue = null; break; } } } finally { unlock(); //解锁.. } return oldValue;
扩容机制
在HashMap中,由于它的底层是用Entry数组存储的, 意味着 resize 是HashMap的方法. 而且不用考虑多线程的问题
而在ConcurrentHashMap中, 虽然扩容的时候是需要lock的, 也就是只有一个线程在扩容, 但是不排除 可能有其他的线程在遍历 , 因此要比HashMap更加的复杂, 整体相似, 也是扩容到原来的2倍容量
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1; //扩容到原来的2倍
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;
if (next == null) //如果单个节点..就直接移动
newTable[idx] = e;
else { //这里就复杂了.... 不像1.7 HashMap 直接使用 头插法 一个一个移过去了
//首先我们知道.. 扩容2倍之后, 就意味着索引要么改变成index+2^n , 要么不变
//根据写源码的人的统计 使用这种方式进行.. 需要克隆的点只有1/6左右 (很强..不知道是怎么做到的)
HashEntry<K,V> lastRun = e; //头节点以及它的idx
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;
/***
太强了这个东西. 这里为什么需要 new ? 为什么 不能直接移动过去?
解 : 因为是个并发容器. 在扩容的期间 不能保证是否 有其他的线程正在读,
不能保证造成的结果 就是 扩容前的表格 是一定要保留下来的, 但是如果直接保留下来的话, 就额外需要非常多的空间, 所以正确的做法是 ,不能够破坏以前的链子结构
不破坏,又要扩容, 那只能 对以前的节点进行克隆移动了
**/
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);
}
}
}
}
//reset结束之后 直接把这个节点插入...
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
图解为何能做到边扩容 边保证 其他线程对扩容前的HashEntry可见 (容量不正确不要在意~举个栗子而已)
如果不需要扩容的话 就直接调用setEntry方法.
static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i,
HashEntry<K,V> e) {
UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
} //这个方法没啥好说的- -..直接使用UNSAFE方法塞进去
只有Put没有Get有何用?
终于弄清楚了put方法… 对于一个map来说…只有put存储…取不出有啥用呢
public V get(Object key) {
//没有任何的锁...非常的自在 , 高并发 就是强
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
//通过(h >>> segmentShift) & segmentMask)获得插入那个插槽, 然后计算出偏移量 u
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//除了都是使用UNSAFE 获取到对象...其他都是正常的寻找....
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;
}
Remove还是需要的
get方法还是简单的…没有什么复杂度. 但是就是因为没有什么复杂度, 又没有加锁… 所以才非常的高效
对于一些不需要的节点…我们还是会去执行remove方法来降低整个map的大小
那么remove到底会怎么样呢? 写写是否会发生互斥 是如何解决多线程的问题…开始
public V remove(Object key) { //remove方法的入口
int hash = hash(key); //直接计算出key的hash 值
Segment<K,V> s = segmentForHash(hash); //通过hash来获取应该在哪个Segment中
return s == null ? null : s.remove(key, hash, null);
}
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;
}
private Segment<K,V> segmentForHash(int h) {
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
}
好! 又转移到了 让Segment 去remove 这个值
这种会引起 Entry 变化的 又要安全的… 通常是需要使用锁的… remove方法也不例外… 开篇现需要trylock一下
if (!tryLock())
scanAndLock(key, hash);
我们先假定没有抢到锁~ 进入 scanAndLock方法
private void scanAndLock(Object key, int hash) {
// similar to but simpler than scanAndLockForPut
//翻译过来就是说 和 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; //最终都会进入到这个地方...要么找到要么找不到..
// emmmm 感觉都是无用功- -反正 扫描一遍完事之后 就开始累加自己的扫描次数
//每2次扫描就会去检查一下当前槽是不是发生改变了, 改变了的话接着扫描...
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;
}
}
}
-
// emmmm ps : 讲道理… 我是真的没搞懂 scan xxx 的方法 到底有啥实际的用途…仅仅只是慢一点抢锁吗? 有大佬知道的话能告诉我一下… 只是减少线程竞争吗
- 继续
当抢到了锁之后, 进入正常的remove 逻辑
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 || //要么直接就是同一个key . 要么就是节点的hash值对上,key的equals方法对上 (注意 : 节点的hash 其实就是key 的hash进行一顿乱移 , 所以还是和 key的hashcode有关)
(e.hash == hash && key.equals(k))) {
V v = e.value; //找到了这个值..
//value为null 意味着直接删除
//反正就是查看一下传入的value 和 这个 v 是否相同
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;
在这个方法中 发现了一个和HashMap区别的地方
在HashMap中 的Entry 的hashcode 方法 是 key的hashcode ^ value的hashcode , 但是在ConcurrentHashMap中, 是使用 把key的hash 一顿移动得到的hashcode
尾声
到这里就已经接近尾声了… map 主要还是 3个方法 put get remove …
剩下的方法 也就是 一些获得迭代器…使用迭代器 调用 map本身的方法
还剩下一个 ConcurrentMap 接口中 得到的 一些replace 方法
public boolean replace(K key, V oldValue, V newValue) {
int hash = hash(key);
if (oldValue == null || newValue == null)
throw new NullPointerException();
Segment<K,V> s = segmentForHash(hash);
return s != null && s.replace(key, hash, oldValue, newValue);
}
public V replace(K key, V value) {
int hash = hash(key);
if (value == null)
throw new NullPointerException();
Segment<K,V> s = segmentForHash(hash);
return s == null ? null : s.replace(key, hash, value);
}
前面部分还是熟悉的部分… 直接交给 Segment 去运行 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; //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;
}
不是总结的总结
这个东西真的很强…
没有总结 - -. 背板也没意思~