ThreadLocal 对于程序员来说,一点也不陌生,为啥?
平常用过,面试也问过。
可 ThreadLocal 也还是有点神秘,内部原理是啥,估计能说清的也没几个。
就比如,ThreadLocal 有 内存溢出 的风险,网上搜索下,大把文章这么说?
我就纳闷了,ThreadLocal 如此优秀,为毛老拿 OOM 说事儿,溢出个毛线呀!
鄙人斗胆,解读下ThreadLocal 的源码(JDK1.8),说说所谓的 “内存溢出”。
如果你对 ThreadLocal 有相当的了解,直接从第四开始看
一、ThreadLocal 是什么先看官方解释
* This class provides thread-local variables. These variables differ from * their normal counterparts in that each thread that accesses one (via its * {@code get} or {@code set} method) has its own, independently initialized * copy of the variable. {@code ThreadLocal} instances are typically private * static fields in classes that wish to associate state with a thread (e.g., * a user ID or Transaction ID). *
大概意思是说:
ThreadLocal 是 线程局部变量。不同于全局变量,这些局部变量是线程私有,并且是各自初始化的变量副本。 ThreadLocal 实例提供静态属性,与 thread 进行关联。
这是英文,翻译过来,不接地气,显得有点别扭。
通俗点解释,线程(thread)有很多属性,比如说线程的名字。
A线程,可以通过 getName() 这个方法,可以得到 A 线程自己的线程名。
B线程,也可以通过 getName() 这个方法,拿到自己的线程名。
这里的线程名就是 线程(thread) 的内存的一个属性,不会有多线程竞争的问题。
线程(thread) 除了 线程名 这个属性之外,还有很多其它的属性,其中有个属性,是一个 Map 类型。
ThreadLocal 用来 *** 作这个 Map,比如 在Map中设置一个值,查找一个值,给 Map 扩容等等。
与线程名一样,这个 Map,不会存在线程竞争问题。
这点我说的啰嗦一点,网上ThreadLocal 的相关文章,很少有说解释ThreadLocal 是什么。
线程高并发时,有时候也会说到 ThreadLocal,但它和高并发没啥关系。
先记住这一句:ThreadLocal 是用来解决,线程间数据隔离问题的。
再打个比方:中国特色的春运抢票,比如说十万个人抢1000张票。
抢票就是一个高并发问题。这里十万个人抢票,大致可理解为 十万个 线程。
1000 张票,是抢夺的对象,谁抢到是谁的,抢到就可以坐高铁回家。
抢票的每个人,都会有个身份z,可以对应理解为线程名,
每个人抢到票,都是要支付的,得有个钱包吧。钱包可以理解为上文说的 thread的那个 Map.
对钱包进行钱的转入转出 *** 作,相当于ThreadLocal ,它封装了相应的方法, *** 作 那个Map。
高并发和 ThreadLocal 有没有啥关系?说有也行,说没有也行。自己瞧着办吧!
上文中,对 ThreadLocal 的官方解释,还有一句
(e.g., a user ID or Transaction ID)
这个说的是 ThreadLocal 的应用场景,比如用户登录后的 userId , 某方法的 事务ID。
我不知道该怎么解释了,好好琢磨下抢高铁票那个例子吧。
二、ThreadLocal 线程安全么?会引起内存溢出么?ThreadLocal *** 作的是 线程的内部属性,不可能有线程间的竞争,也不没有所谓线程安全不安全的事儿了。
再强调一次:ThreadLocal 是用来解决,线程间数据隔离问题的。
Threadlocal 会引起内存溢出么?可以放心使用么?
这个问题先放一放,我问你,平常手机充电,你用的插座安全吗?
估计你会说:都在市场上卖了这么多年,全球几亿人用了百十年了,当然是安全的呀!
那我再问你,那朝着插座撒泡尿,安全?把圆规的两个尖儿插到插座的两个插孔里,安全?
我很负责任的警告你:
朝着插座撒尿,警察蜀黍不会因为有伤风化,把你带到小黑屋。
然而, 马克思会亲自接见你,真的!
把圆规尖扎到插座孔里,你能直接见耶稣!!!
插座是安全的,前提是你正常使用它。你硬要非主流使用它,耶稣也救不了你。
同样,Threadlocal 正常不会引起内存溢出。你非要zuo,那溢出了怪谁。
到底怎样,ThreadLocal 才会引起内存溢出呢?
前面说过,Thread 的有个内存的属性,类型是 个 Map, ThreadLocal 是 *** 作这个 Map的。
这个 Map 的 Key 是 ThreadLocal 实例的内存地址,value 是要存入的值。
是 Map 就会有个容量的极限值,超过这个值,那可不就把 Map 给撑破了。
说白了就是一直往这个Map里塞值,直到塞爆它,我写了个 demo
public static void main(String[] args) { int max = Integer.MAX_VALUE; for(int i = 0; i < max; i++){ ThreadLocal threadLocal = new ThreadLocal(); threadLocal.set("测试thread属性" + i); if(i % 1000 == 0){ System.out.println("set 中 " + i); } } System.out.println("测试结束"); Thread thread = Thread.currentThread(); }
执行上面那段代码后,我去吃午饭,想着让它慢慢跑
吃了快一个小时,回来代码仍旧在执行中,并没有到 OOM 的程度。
这就是很 zuo 死的代码,玩儿了命的往 Map 里塞东西,即便这样,执行了几十分钟,没有内存溢出。
俗话又说,饭前便后要洗手,即便你没洗手,也不是一定会生病。
上面那段很 zuo 的代码,就相当于没洗手, 应该加一句 threadLocal.remove();
这样,即便循环改为 long 的最大值,执行到天荒地老,也不会 OOM。(为什么,后面分析源码再细说)
ThreadLocal 放心大胆的用,没有问题。你创建的 ThreadLocal 实例,能有几个,不会把 Map 撑破。
那 ThreadLocal 到底会不会引起内存溢出?
那你先说插座安全不安全?
你要说插座安全,那 ThreadLocal 就不会引起内存溢出;
你要说插座不安全,那 ThreadLocal 会引起内存溢出。
这样的回答,您还满意不???
三、ThreadLocal 的使用前面的例子中说过,Thread 中有个属性是 Map 类型, ThreadLocal 就是 *** 作这个Map
public class Thread implements Runnable { private volatile String name; ThreadLocal.ThreadLocalMap threadLocals = null; …… }
这是 Thread 类 摘录的两行代码,threadLocals 就是刚刚说的那个 Map,
它是定义在 ThreadLocal 中的一个内部类。然后我们来看 ThreadLocal 的使用
public static void main(String[] args) { ThreadLocalthreadLocal = new ThreadLocal(); threadLocal.set("测试thread属性"); String value = threadLocal.get(); threadLocal.remove(); Thread thread = Thread.currentThread(); }
这个很简单一个示例,创建一个 ThreadLocal 的实例,设置一个值。看下 set 源码。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
代码很简洁,getMap(t) 这个方法,就是拿到 Thread 类中的属性 threadLocals,就是那个 Map。
- Map 存在,设置 key, value。
- Map 不存在,创建Map,然后再设置 key, value。
再看 threadLocal.get() 的源码
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
代码很简洁,先拿到 Map,再看对应的 key 值 能不能取到 Entry,能拿到,返回 value。
其它情况返回 setInitialValue()——这个方法太简单,不值得讲,自己看吧,其最终是返回 null。
remove() 这个方法源码,涉及更深层的东西,等会再讲。记住 它是把 Map 中 key 值 设置为 null.
四、弱引用static class ThreadLocalMap { static class Entry extends WeakReference> { Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } } …… }
ThreadLocalMap 这个是定义在 ThreadLocal 类中的一个内部类。特殊之处是 key 是弱引用。
- 从哪里看出是弱引用的?
... extends WeakReference 这里写的很清楚。
- key 是弱引用,有啥用?
设想这么一个场景,用线程池的时候,
Thread 是有可能一直存活,那它的属性 threadLocals 会一直存在。
当这个线程在执行 A 方法时,创建了一个 ThreadLocal 实例 a,set 了一个值,当 A 方法结束后,a 对象被 GC 回收。
随后,该线程又执行了 B 方法,假设也创建了 ThreadLocal 实例 b,set 了值,B 方法结束时,b 对象被 GC 回收。
对于线程来说,不管 实例 a、实例 b 是否被 GC 回收,在 threadLocals 这个Map 里,会有其相应的两个 Entry。
情况就是 实例a 实例 b 已被GC 回收,但线程中,对应的 Entry 却依然存在,典型 占着茅坑不拉屎。
在源码的注释中,管这样的节点叫 “stale entries” ,本篇咱就叫它失效节点。
这样的节点可能会越来越多,直到 OOM。
ThreadLocalMap 中 key 设计为 弱引用,就是针对 “stale entries”,当 实例 a 、实例 b 被 GC 回收后,
其对应的 Entry 的 key 就是 null,会有专门的方法清除这样的结点,从而避免 OOM。
不过话又说回来了,内存溢出没那么容易,开篇我写了个 demo,执行了几十分钟,没有 OOM。
int 的最大值,是个很大的数字了。
好了,至此为止,ThreadLocal 讲的内容,应对面试已经没问题了。
下面开始更深入的解析源码,很枯燥,没耐心的,可以劝退啦哈!
五、魔数 0x61c88647ThreadLocalMap 是个 Map,
初始容量是 16,之后扩容,新的容量是旧容量的 2倍。
即容量一定是 2 的 n 次方
对应的 哈希函数 是 一个数字 & (容量 - 1)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 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;
前面写过一篇文章《hash & (n - 1) 是什么》,本篇不再做解释。
ThreadLocalMap 的哈希函数,是 从 0 开始,每次加 0x61c88647 ,再和 (容量-1)做位运算。
我写了代码模拟这个过程,看到结果后,我大受震撼!!!
public static void main(String[] args) { int count = 64; Listlist = new ArrayList<>(count); AtomicInteger nextHashCode = new AtomicInteger(); int HASH_INCREMENT = 0x61c88647; for(int i = 0; i < count; i++){ int hash = nextHashCode.getAndAdd(HASH_INCREMENT); int index = hash & (count -1); list.add(index); } System.out.println(list); } -----------------打印结果如下------------------------ [0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 6, 13, 20, 27, 34, 41, 48, 55, 62, 5, 12, 19, 26, 33, 40, 47, 54, 61, 4, 11, 18, 25, 32, 39, 46, 53, 60, 3, 10, 17, 24, 31, 38, 45, 52, 59, 2, 9, 16, 23, 30, 37, 44, 51, 58, 1, 8, 15, 22, 29, 36, 43, 50, 57]
没有哈希冲突,0x61c88647 这个数字为什么可以做到没有哈希冲突。
我无法表达我的感受,贴张图意思一下。
HASH_INCREMENT = 0x61c88647,它对应的 十进制 数字是 1640531527,它就是魔数。
为什么每次加上它之后,算出来的哈希值没有冲突,且分布均匀?
我上网搜索了下,网上是这么说的。
2 的 32 次方,这个数字与( 1 - 黄金分割比例)的乘积,就是 魔数 1640531527。
数学上的证明,和 斐波那契数列 相关,反正我不懂!
在此不仅感叹, Doug Lea 是 ThreadLocal 的作者之一,
不仅高并发的工具类写的好,高数也应该很好,人与人的差别怎么就这么大呢?
六、set 源码精讲public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 拿到 Map if (map != null) map.set(this, value); else createMap(t, value); // 自己看源码吧,记住初始容量是 16 }
这段没啥可讲的,直接看 map.set(this, value);
private void set(ThreadLocal> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
代码中的 for 循环可以大致理解为:清理无效节点(上文说的 stale entries)。这个最后再讲。
另:for 循环中里的代码,处理了key存在的情况,
扣除 for 循环,剩下的代码功能也是完整的。(entry 不存在的情况)
private void set(ThreadLocal> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); // 算出下标 tab[i] = new Entry(key, value); // 设置 entry int sz = ++size; // 记录 entry 的个数 if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清理无效节点 rehash(); // 清理无效节点失败且达到扩容阈值时,启动扩容 }
先看 rehash() 方法,再细说 cleanSomeSlots(i, sz)
private void rehash() { expungeStaleEntries(); if (size >= threshold - threshold / 4) resize(); } private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal> k = e.get(); if (k == 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; }
resize() 方法还是很容易理解的 。摘录其中一段来说明
Entry e = oldTab[j]; if (e != null) { ThreadLocal> k = e.get(); if (k == 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++; // 记录节点的个数 } } private static int nextIndex(int i, int len) { // 如果下一个节点是最后一个节点,返回第一个节点 return ((i + 1 < len) ? i + 1 : 0); }
k == null 这个分支,就是 entry 存在,但是返回对应的key,即 ThreadLocal 实例的地址不存在了,那就回收该节点。这就是所谓的 无效节点的清除。
else 分支,就是正常节点,先通过哈希函数,算出下标。(这个在魔数那讲过了)
如果出现了哈希冲突,即该节点已被占用,那就判断下一个节点,直到找到一个空节点,迁移新节点。
哈希冲突解决办法,常见有两种
- 链表法,比如 HashMap
- 开放寻址法,比如今天所讲的 ThreadLocal
rehash() 方法,还剩下 expungeStaleEntries() 没讲
private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { // 遍历所有节点,发现 无效节点,清除 Entry e = tab[j]; if (e != null && e.get() == null) // entry 存在,但 key 不存在 expungeStaleEntry(j); } } private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; // 清空 vaule tab[staleSlot] = null; // 清空 该节点 size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
expungeStaleEntry(j) 这个方法,是在判断该位置是 无效节点时调用的。
清除该节点之后,工作已经做完了。它又来了个 for 循环。还是清除节点。
我画了张图,来说明问题,比如调用 expungeStaleEntry(j) 时,j = 1。
那 for 循环那段代码,就不进去,因为 下标 为 2 的那个节点是空节点。
假如调用 expungeStaleEntry(j) 时,j = 4,
那 for 循环将处理 5 至 8 四个结点(遇到空节点退出for循环)
5 6 7 这三个节点,会进第一个分支 if (k == null) ,清除无效节点。
节点 8 进入 else 分支。此时 i = 8
int h = k.threadLocalHashCode & (len - 1); // 重新计算下标 if (h != i) { // 说明需要移动位置 tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; }
假如 重新计算下标,得到 h = 4,此时,先将下标为8的节点设置为空。
再从下标为 4 开始向后遍历,直到找到空节点,将 该节点指向 entry。
至此 rehash 方法讲完了。
- cleanSomeSlots(i, sz)
回过头再看下,清除节点失败,且达到阈值,才调用 rehash() 方法启动扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
cleanSomeSlots 这个方法有点复杂,我慢慢说
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; // 默认没有无效节点 Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
调用 cleanSomeSlots(i, sz) 时,i 这个下标,设置一个新的 entry,sz 指数组中 entry 的总个数。
do { …… } } while ( (n >>>= 1) != 0);
这个循环,时间复杂度,一定不是 O(n),应该是 O(log n)。
如果 n = 2 的 10 次方,那 n >>>= 1 得到就是 2 的 9 次方。
即 这个循环,会执行 10 次,然后再看循环里的代码
i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; // 重置 n removed = true; // 标记下扫描到了失效节点 i = expungeStaleEntry(i); // 上文讲过这个方法了。 }
这里当扫描到 失效节点时,会重置 n,
即扫描到失效节点,就扩大扫描范围,这个不细讲了,自己体会吧。
expungeStaleEntry 这个方法,上面详细讲过了,没有印象的话,翻回去再看看。
至此 set 方法讲了一半,(entry 不存在的情况)
总结下逻辑: entry 不存在时,
- 创建新节点。
- 扫描下一个节点,如果是失效节点,清除,并扩大扫描范围。
- 符合条件时,调用 rehash 方法,刷新整个数组,清除失效节点。
- 必要时启动扩容,数组容量翻倍。
下面讲另一半的源码,entry 存在时的逻辑
先明确一点,这种情况下,一定有空节点的存在。
为什么?你往上翻翻 rehash 方法,没有空节点时,一定会触发扩容。
Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } }
if (k == key) 这个分支的代码,很好理解,直接设值,结束。
if (k == null) 这个分支,指这是一个失效节点,调用 replaceStaleEntry,
此方法第三个参数,失效节点 的下标。
private void replaceStaleEntry(ThreadLocal> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; // slotToExpunge需要清除的节点下标 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
这是一个能把人看哭的代码,不好理解,我慢慢说。
先记住这个方法第三个参数 staleSlot,是要失效的节点的下标。
slotToExpunge 是要清除的节点下标,初始值是 staleSlot。
int slotToExpunge = staleSlot; // slotToExpunge需要清除的节点下标 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) // 是失效节点 slotToExpunge = i;
这个是后退循环,遇到空节点时退出。(一定会存在空节点,前面说过了),
在此期间,遇到失效节点,就重置 slotToExpunge
借用前面的那张图,假如 调用 replaceStaleEntry 方法时,staleSlot 是 5
扫描下标为 4 的结点,不是失效节点,进入下个循环。
下标为 3 的结点是空节点,跳出循环,此时slotToExpunge 还是5
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal> k = e.get(); if (k == key) { …… return; } if (k == null && slotToExpunge == staleSlot) // 遇到失效节点,且后退循环没找到失效节点 slotToExpunge = i; }
这是个正循环,扫描到 空节点退出循环。结合上图,该循环,可能扫描 6 7 8 这三个节点,
扫描到下标为 9 的结点,退出循环。
在此期间,
- 如果遇到同样的 key ,执行一些 *** 作,结束。
- 如果遇到失效结点,且后退循环没有遇到失效节点,重置 slotToExpunge,直到跳出循环后,执行某些 *** 作,结束。
先讲 遇到同样的key,这种情况
if (k == key) { e.value = value; // 设置值。 // e 与 tab[staleSlot] 互换位置 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; }
还以上面的图来说,假设 i = 8 时的那个循环,进入了这个分支。
下标为5的节点,与下标为8 的结点,互换位置。
即下标为8的节点,是失效节点,下标为5的位置,是 存着 key 与 vaule。
if (slotToExpunge == staleSlot) 成立,slotToExpunge 的值变为8。
(不成立时,自己想想吧,不明白可以留言哈)
执行 expungeStaleEntry(slotToExpunge),即清除下标为8的结点,这方法上面讲过了。
执行 cleanSomeSlots,即扫描后继结点,必要情况下扩大扫描范围。上面也讲过了。
至此,遇到同样的key的情况讲完了,现在讲另外的逻辑——没有遇到同样的key
tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
前两行很容易理解,没有遇到同样的key,那直接在 staleSlot 这个下标的位置,
将旧的 value 清除,设置新的 key 和 value。
if (slotToExpunge != staleSlot) 这句的意思是,后退的那个 for 循环,扫描到了失效节点,
那执行expungeStaleEntry 清除该节点, 执行 cleanSomeSlots 进行后继节点的扫描。
总结下逻辑: entry 存在时
- 遇到同样的key,覆盖旧值,结束。
- 遇到的是失效节点,在后继节点,找到同样的key,交换,清除无效节点。
- 后继节点没找到同样的key,直接将失效节点转换掉。
顺便说一句: entry 存在时,代码很复杂,是因为 哈希冲突时,采用线性探测的方法来解决。
至此 set 方法讲完了,分为两种情况,entry 存在,与 entry 不存在两种情况。
代码复杂是因为两个方法:线性探测 和 清除无效点的处理。
七、get 方法略讲public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); // 拿到 Map if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); // 这个源码不用看,反正是返回null }
这段代码很清晰,只讲 getEntry 方法
private Entry getEntry(ThreadLocal> key) { int i = key.threadLocalHashCode & (table.length - 1); // 计算下标 Entry e = table[i]; if (e != null && e.get() == key) // entry 存在且 key 相同 return e; else return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
getEntryAfterMiss 这个方法中,while 循环,是一直往后找,
直到找到相同的key,或者遇到 空节点退出。
期间如果遇到失效节点,执行 expungeStaleEntry 清除失效节点。
总体代码不难,也是解决 线性寻址 的问题,和清理无效节点的问题。
八、remove 方法略讲public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } } public void clear() { this.referent = null; }
调用 remove 方法,逻辑就是找到对应的 entry,
清除 key,即entry 变为失效节点,再清理失效节点。
若 key 相等,
九、小结有没有发现,ThreadLocal 中,没有 CAS *** 作,也没有用锁,不会产生并发问题么?
不会!!!
map 是线程本身的属性,和线程名一样,每个线程都有,各自用各自的,不用抢。
set、 get、 remove 这三个方法,都会清理失效节点,
所以用 ThreadLocal时,大可不必担心,内存溢出的问题。正常使用,没问题的。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)