1.ThreadLocal简介及案例
通常情况下,我们创建的变量可以被任何一个线程访问并修改(线程之间可以通过主内存中转来访问并修改对方工作内存的变量),但是如果我们要实现每一个线程都有自己的专属本地变量该如何解决?。JDK提供ThreadLocal 类主要解决就是让每一个线程绑定自己的值(其他线程通过主内存中转也修改不了对方变量),可以将ThreadLocal 类对象比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据,相当于缓存。
自定义简单的ThreadLocal案例:
public class ThreadlocalDemo {
//创建ThreadLocal对象
ThreadLocal<String> t1= new ThreadLocal<String>();
private String content;
public String getContent() {
String s = t1.get();
return s;
}
public void setContent(String content) {
t1.set(content);
}
public static void main(String[] args) {
ThreadlocalDemo demo=new ThreadlocalDemo();
//5个线程0-->4
for (int i = 0; i < 5; i++) {
//实现Runnable接口创建线程:先创建线程,再通过new Runnable()执行run任务
//创建线程和执行任务分开
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//每个线程存一个变量,过一会儿,再取出这个变量
demo.setContent(Thread.currentThread().getName()+"的数据");
System.out.println(Thread.currentThread().getName()+"-->"+demo.getContent());
System.out.println("---------------------------");
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
/**
* 线程1-->线程1的数据
* ---------------------------
* 线程3-->线程3的数据
* ---------------------------
* 线程2-->线程2的数据
* ---------------------------
* 线程4-->线程4的数据
* ---------------------------
* 线程0-->线程0的数据
* ---------------------------
* */
2. ThreadLocal的内部结构
2.1 ThreadLocal的特点
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通 过get和set方法访问)时能**保证各个线程的变量相对独立于其他线程内的变量。**ThreadLocal实例通常来说都是 private static类型的,用于关联线程和线程上下文。
我们可以得知ThreadLocal的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在 线程的生命周朗内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度.
特点 | 内容 |
---|---|
1.线程并发 | 在多线程并发场景下 |
2.传递数据 | 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量 |
3.线程隔离 | 每个线程的变量都是独立的, 不会互相影响.(核心) |
2.2 ThreadLocal 和Synchronized的区别
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间的方式,只提供了一份变量, 让不同的线程排队访问 | ThreadLocal采用以空间换时间的方式, 为每一个线程都提供了一份变量的副本, 从而实现同访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
2.3 内部结构
JDK 早期设计
每个ThreadLocal都创建一个Map(ThreadLocalMap)
,
这里的ThreadLocalMap和HashMap类似,用Thread(线程) 作为Map的key
, 要存储的局部变量作为Map的value
, 这样就能达到各个线程的局部变量隔离的效果, 这是最简单的设计方法. 早期设计
JDK 8 设计
JDK8中ThreadLocal的设计是 : 每个Thread维护一个ThreadLocalMap, 这个Map的key
是ThreadLocal实例本身,value
才是真正要存储的值Object
两者对比
JDK8与早期的版本最大的区别就是key的变化。早期版本key是Thread线程,JDK8之后key是ThreadLocal。
JDK 8 这样设计的好处:
- 每个Map存储的Entry数量变少。因为早期版本的key是Thread线程,如果线程数很多,那么Entry数量也会变多,Entry数量是由key来决定的。而8之后key变成ThreadLocal,这个数量通常很少。
- 当Thread销毁的时候, THreadLocalMap也会随之销毁, 减少内存的使用。早期是由ThreadLocal维护ThreadLocalMap,所以生命周期很长,现在以Thread维护一个ThreadLocalMap,当线程执行结束就会销毁,所以生命周期短。
3.ThreadLocal源码分析
- ThreadLocal里面的内部类是ThreadLocalMap和SuppliedThreadLocal;
- ThreadLocalMap里面的内部类是Entry类;
- Entry类继承弱引用WeakReference<ThreadLocal<?>>
ThreadLocal中的嵌套内部类ThreadLocalMap,这个类本质上是一个map,和HashMap之类的实现相似,依然是key-value的形式,其中有一个内部类Entry,其中key在JDK8之后可以看做是ThreadLocal实例,但是其本质是持有ThreadLocal实例的弱引用
3.1 ThreadLocal中的set()及其相关方法
代码执行流程:
- A.首先获取当前线程,并根据当前线程获取一个ThreadLocalMap对象----
getMap(t)
- B. 如果获取的ThreadLocalMap对象不为空,则将参数设置到ThreadLocalMap中(当前ThreadLocal作为key,变量副本为value)----
map.set(this, value);
- C. 如果ThreadLocalMap对象为空,则给该线程创建ThreadLocalMap,并设置初始值,作为Map的Entry数组的第一个元素----
createMap(t, value);
/**
*设置当前线程对应的ThreadLocalMap对象的value值=变量副本,而key=ThreadLocal对象
*@param value 将要保存在当前线程维护的ThreadLocalMap对象的value值=变量副本
*/
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取Thread线程中维护的ThreadLocalMap对象
ThreadLocal.ThreadLocalMap map = getMap(t);
//判断ThreadLocalMap对象是否存在
if (map != null)
/*存在就去调用ThreadLocalMap的set()方法
进入ThreadLocalMap的set();
this指key=ThreadLocal, value=变量的副本值
作为ThreadLocalMap存放的key-value*/
map.set(this, value);
else
//不存在ThreadLocalMap对象就去创建该Map对象
//t是Thread线程,value是变量的副本
createMap(t, value);
}
//获取当前Thread线程维护的一个ThreadLocalMap对象
//t当前线程, return返回的是ThreadLocalMap对象
ThreadLocal.ThreadLocalMap getMap(Thread t) {
//进入Thread的threadLocals变量去创建map对象
return t.threadLocals;
}
//创建Map并初始化
void createMap(Thread t, T firstValue) {
//this指key=ThreadLocal对象,firstValue副本变量
//作为ThreadLocalMap存放的第一个key-value
t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}
createMap中构造函数ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//计算索引:key的哈希值与数组长度-1进行位运算得到索引值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//设置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
关于firstKey.threadLocalHashCode
计算索引值:AtomicInteger
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
ThreadLocalMap的引用在Thread类中
public
class Thread implements Runnable {
......
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
......
3.2 ThreadLocal中的get()及其相关方法
代码执行流程:
- A. 首先获取当前线程,并根据当前线程获取一个ThreadLocalMap对象----
getMap(t)
- B. 如果获取的ThreadLocalMap对象不为空,则在Map中以ThreadLocal的引用作为key在Map中获取对应的存储的实体Entry e,否则转到D----
map.getEntry(this);
- C. 如果e不为null,则返回e.value,否则转到D----
T result = (T)e.value;
- D. Map为空或者e为空,则通过
setInitialValue()
获取Map的value值,该方法的过程与set()方法类似:创建Map对象,然后把key=ThreadLocal和value=变量副本值放入Entry数组(通过key计算哈希值,再由哈希值与数组长度-1位运算得出索引值存入数组,并把对应value也放入)
总结:先获取当前线程的ThreadLocalMap对象,如果存在,返回该Map对应的value值,不存在则创建ThreadLocalMap对象并返回value值
/**
*返回当前线程Thread维护的ThreadLocalMap对象的value值=变量副本值
*两种路径: (1) T result = (T)e.value,return result
*(2) return value
*/
public T get() {
//同set方法类似,先获取当前线程
Thread t = Thread.currentThread();
//获取Thread线程中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//判断ThreadLocalMap对象是否存在
if (map != null) {
//存在map对象,就以this=ThreadLocal作为map的key,
//调用getEntry(this)方法得到存储的实体Entry e
ThreadLocalMap.Entry e = map.getEntry(this);
//对存储实体e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
//获取变量副本值value,Entry是一个哈希桶数组
T result = (T)e.value;
return result;
}
}
/**
*初始化:有两种情况可以执行当前的代码
*第一种:ThreadLocalMap不存在,表示此线程没有维护的ThreadLocalMap对象
第二种:ThreadLocalMap存在,但是存储实体e不存在,没有关联到当前ThreadLocal(key)
*/
//返回变量副本值value
return setInitialValue();
}
/**
* 初始化设值的方法,可以被子类覆盖。
*/
protected T initialValue() {
return null;
}
//获取变量副本值value
private T setInitialValue() {
//获取初始化值,默认为null(如果没有子类进行覆盖)
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
//获取Thread线程中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//判断ThreadLocalMap对象是否存在
if (map != null)
/*存在就去调用ThreadLocalMap的set()方法
进入ThreadLocalMap的set();
this指key=ThreadLocal, value=变量的副本值
作为ThreadLocalMap存放的key-value*/
map.set(this, value);
else
//不存在ThreadLocalMap对象就去创建该Map对象
//第一次初始化,createMap在上面介绍set()的时候有介绍过。
//t是Thread线程,value是变量的副本
createMap(t, value);
//获取变量副本值value
return value;
}
3.3 ThreadLocal中的 remove()
执行流程:
- A . 首先获取当前线程维护的ThreadLocalMap对象
- B. 如果ThreadLocalMap对象存在,则移除当前ThreadLocal对应的实体Entry
/**
*删除当前线程中保存的ThreadLocal对应的实体Entry
*/
public void remove() {
//获取当前线程维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
//判断ThreadLocalMap对象是否为空
if (m != null)
//不为空就移除this=ThreadLocal(key),这样对应的实体Entry也删除了
m.remove(this);
}
3.4 ThreadLocal中的 initialValue()
此方法在get()方法里也有,作用是:返回该线程局部变量的初始值
(1) 这个方法是一个延迟调用的方法,从上面的代码得知,在set()方法还未调用而先调用了get()方法才执行,并且只执行一次
(2)这个方法实现直接返回一个null;
(3)如果想要一个除null之外的初始值,可以重写次方法,这个方法显然需要子类方法去实现的。
protected T initialValue() {
return null;
}
4. ThreadLocalMap
4.1 ThreadLocalMap结构
ThreadLocalMap是ThreadLocal的静态内部类, 没有实现Map接口, 用独立的方式实现了Map的功能, 其内部的Entry也是独立实现.
- ThreadLocal的set()方法调用了ThreadLocalMap的set()方法
- ThreadLocal的get()方法里调用了ThreadLocalMap的getEntry()方法
- ThreadLocal的remove()方法调用了ThreadLocalMap的remove()方法
4.2 成员变量
/**
* The initial capacity -- MUST be a power of two.
* 初始化容量,必须是2的整数次幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table, 同样数组长度必须是2的整数次幂
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* 数组里entrys的个数,可以判断table是否超过阈值 (存储的格式)
* The number of entries in the table.
*/
private int size = 0;
/**
* 阈值 进行扩容的阈值,表使用大于他的时候,进行扩容
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* 定义为长度的2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
4.3 存储结构-Entry
Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收。
/**
* Entry继承WeakReference,并且用ThreadLocal作为key.如果key为null
* (entry.get() == null)表示key不再被引用,表示ThreadLocal对象被回收
* 因此这时候entry也可以从table从清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4.4 ThreadLocalMap中的set()
ThreadLocalMap使用线性探测法
来解决哈希冲突,线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
/**java
/**
* 获取环形数组的下一个索引
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 获取环形数组的上一个索引
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap的set()及其set()相关代码如下:
private void set(ThreadLocal<?> key, Object value) {
//获取存放数据的Entry数组,哈希桶数组
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//计算索引,上面已经有说过。
int i = key.threadLocalHashCode & (len-1);
/**线性探测法
* 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,
* 就使用nextIndex()获取下一个(上面提到到线性探测法)。
*/
//for循环:初始值tab[i],条件:e != null,自增值nextIndex()获取下一个
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//table[i]上key不为空,并且和当前key相同,覆盖之前的value
if (k == key) {
e.value = value;
return;
}
/**
* table[i]上的key为空,说明被回收了(上面的弱引用中提到过),此时的Entry数组存放的value就是一个旧值。
* 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry,防止内存泄露
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//上面的for循环都不满足,因此没有return结束,进入下面代码
//找到为空的插入位置,插入值,在为空的位置插入需要对size进行加1操作
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
/**
* cleanSomeSlots用于清除那些e.get()==null,e.get()就是key(ThreadLocal),也就是table[index] != null && table[index].get()==null
* 之前提到过,这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
* 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/**
* 替换无效entry
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
ThreadLocal.ThreadLocalMap.Entry e;
/**
* 根据传入的无效entry的位置(staleSlot),向前扫描
* 一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
* 直到找到一个无效entry,或者扫描完也没找到
*/
int slotToExpunge = staleSlot;//之后用于清理的起点
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
/**
* 向后扫描一段连续的entry
*/
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
/**
* 如果找到了key,将其与传入的无效entry替换,也就是与table[staleSlot]进行替换
*/
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//如果向前查找没有找到无效entry,则更新slotToExpunge为当前值i
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
/**
* 如果向前查找没有找到无效entry,并且当前向后扫描的entry无效,则更新slotToExpunge为当前值i
*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
/**
* 如果没有找到key,也就是说key之前不存在table中
* 就直接最开始的无效entry——tab[staleSlot]上直接新增即可
*/
tab[staleSlot].value = null;
tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
/**
* slotToExpunge != staleSlot,说明存在其他的无效entry需要进行清理。
*/
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 连续段清除
* 根据传入的staleSlot,清理对应的无效entry——table[staleSlot],
* 并且根据当前传入的staleSlot,向后扫描一段连续的entry(这里的连续是指一段相邻的entry并且table[i] != null),
* 对可能存在hash冲突的entry进行rehash,并且清理遇到的无效entry.
*
* @param staleSlot key为null,需要无效entry所在的table中的索引
* @return 返回下一个为空的solt的索引。
*/
private int expungeStaleEntry(int staleSlot) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 清理无效entry,置空
tab[staleSlot].value = null;
tab[staleSlot] = null;
//size减1,置空后table的被使用量减1
size--;
ThreadLocal.ThreadLocalMap.Entry e;
int i;
/**
* 从staleSlot开始向后扫描一段连续的entry
*/
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果遇到key为null,表示无效entry,进行清理.
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//如果key不为null,计算索引
int h = k.threadLocalHashCode & (len - 1);
/**
* 计算出来的索引——h,与其现在所在位置的索引——i不一致,置空当前的table[i]
* 从h开始向后线性探测到第一个空的slot,把当前的entry挪过去。
*/
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
//下一个为空的solt的索引。
return i;
}
/**
* 启发式的扫描清除,扫描次数由传入的参数n决定
*
* @param i 从i向后开始扫描(不包括i,因为索引为i的Slot肯定为null)
*
* @param n 控制扫描次数,正常情况下为 log2(n) ,
* 如果找到了无效entry,会将n重置为table的长度len,进行段清除。
*
* map.set()点用的时候传入的是元素个数,replaceStaleEntry()调用的时候传入的是table的长度len
*
* @return true if any stale entries have been removed.
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
ThreadLocal.ThreadLocalMap.Entry e = tab[i];
if (e != null && e.get() == null) {
//重置n为len
n = len;
removed = true;
//依然调用expungeStaleEntry来进行无效entry的清除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//无符号的右移动,可以用于控制扫描次数在log2(n)
return removed;
}
private void rehash() {
//全清理
expungeStaleEntries();
/**
* threshold = 2/3 * len
* 所以threshold - threshold / 4 = 1en/2
* 这里主要是因为上面做了一次全清理所以size减小,需要进行判断。
* 判断的时候把阈值调低了。
*/
if (size >= threshold - threshold / 4)
resize();
}
/**
* 扩容,扩大为原来的2倍(这样保证了长度为2的冥)
*/
private void resize() {
ThreadLocal.ThreadLocalMap.Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
ThreadLocal.ThreadLocalMap.Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
//虽然做过一次清理,但在扩容的时候可能会又存在key==null的情况。
if (k == null) {
//这里试试将e.value设置为null
e.value = null; // Help the GC
} else {
//同样适用线性探测来设置值。
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//设置新的阈值
setThreshold(newLen);
size = count;
table = newTab;
}
/**
* 全清理,清理所有无效entry
*/
private void expungeStaleEntries() {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
ThreadLocal.ThreadLocalMap.Entry e = tab[j];
if (e != null && e.get() == null)
//使用连续段清理
expungeStaleEntry(j);
}
}
4.5 ThreadLocalMap中的getEntry()及其相关
private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
//根据key计算索引,获取entry
int i = key.threadLocalHashCode & (table.length - 1);
ThreadLocal.ThreadLocalMap.Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* 通过直接计算出来的key找不到对于的value的时候适用这个方法.
*/
private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> key, int i, ThreadLocal.ThreadLocalMap.Entry e) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//清除无效的entry
expungeStaleEntry(i);
else
//基于线性探测法向后扫描
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
4.6 ThreadLocalMap中的remove()
private void remove(ThreadLocal<?> key) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
//计算索引
int i = key.threadLocalHashCode & (len-1);
//进行线性探测,查找正确的key
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
//调用weakrefrence的clear()清除引用
e.clear();
//连续段清除
expungeStaleEntry(i);
return;
}
}
}