⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 https://mp.weixin.qq.com/s/K-8aNF3gqg3ekrRbTsjo9w 「CR7」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

Hello World

在展开深入分析之前,咱们先来看一个官方示例:

出处来源于ThreadLocal类上的注释,其中main方法是笔者加上的。

 1import java.util.concurrent.atomic.AtomicInteger;
2
3public class ThreadId {
4 // Atomic integer containing the next thread ID to be assigned
5 private static final AtomicInteger nextId = new AtomicInteger(0);
6
7 // Thread local variable containing each thread's ID
8 private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
9 @Override
10 protected Integer initialValue() {
11 return nextId.getAndIncrement();
12 }
13 };
14
15 // Returns the current thread's unique ID, assigning it if necessary
16 public static int get() {
17 return threadId.get();
18 }
19
20 public static void main(String[] args) {
21 for (int i = 0; i < 5; i++) {
22 new Thread(new Runnable() {
23 @Override
24 public void run() {
25 System.out.println("threadName=" + Thread.currentThread().getName() + ",threadId=" + ThreadId.get());
26 }
27 }).start();
28 }
29 }
30}

运行结果如下:

1threadName=Thread-0,threadId=0
2threadName=Thread-1,threadId=1
3threadName=Thread-2,threadId=2
4threadName=Thread-3,threadId=3
5threadName=Thread-4,threadId=4

我问:看完这个例子,您知道ThreadLocal是干什么的了吗? 您答:不知道,没感觉,一个hello world的例子,完全激发不了我的兴趣。 您问:那个谁,你敢不敢举一个生产级的、工作中真实能用的例子? 我答:得,您是"爷",您说啥我就做啥。还记得《Spring Cloud Netflix Zuul源码分析之请求处理篇》中提到的RequestContext吗?这就是一个生产级的运用啊。Zuul核心原理是什么?就是将请求放入过滤器链中经过一个个过滤器的处理,过滤器之间没有直接的调用关系,处理的结果都是存放在RequestContext里传递的,而这个RequestContext就是一个ThreadLocal类型的对象啊!!!

 1public class RequestContext extends ConcurrentHashMap<String, Object> {
2
3 protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
4 @Override
5 protected RequestContext initialValue() {
6 try {
7 return contextClass.newInstance();
8 } catch (Throwable e) {
9 throw new RuntimeException(e);
10 }
11 }
12 };
13
14 public static RequestContext getCurrentContext() {
15 if (testContext != null) return testContext;
16
17 RequestContext context = threadLocal.get();
18 return context;
19 }
20}

以Zuul中前置过滤器DebugFilter为例:

 1public class DebugFilter extends ZuulFilter {
2
3 @Override
4 public Object run() {
5 // 获取ThreadLocal对象RequestContext
6 RequestContext ctx = RequestContext.getCurrentContext();
7 // 它是一个map,可以放入数据,给后面的过滤器使用
8 ctx.setDebugRouting(true);
9 ctx.setDebugRequest(true);
10 return null;
11 }
12}

您问:那说了半天,它到底是什么,有什么用,能不能给个概念? 我答:能!必须能!!!

What is this

它是啥?它是一个支持泛型的java类啊,抛开里面的静态内部类ThreadLocalMap不说,其实它没几行代码,不信,您自己去看看。它用来干啥?类上注释说的很明白:

  • 它能让线程拥有了自己内部独享的变量
  • 每一个线程可以通过get、set方法去进行操作
  • 可以覆盖initialValue方法指定线程独享的值
  • 通常会用来修饰类里private static final的属性,为线程设置一些状态信息,例如user ID或者Transaction ID
  • 每一个线程都有一个指向threadLocal实例的弱引用,只要线程一直存活或者该threadLocal实例能被访问到,都不会被垃圾回收清理掉

爱提问的您,一定会有疑惑,demo里只是调用了ThreadLocal.get()方法,它如何实现这伟大的一切呢?这就是笔者下面要讲的内容,走着~~~

我有我的map

话不多说,我们来看get方法内部实现:

get()源码

 1public T get() {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null) {
5 ThreadLocalMap.Entry e = map.getEntry(this);
6 if (e != null) {
7 @SuppressWarnings("unchecked")
8 T result = (T)e.value;
9 return result;
10 }
11 }
12 return setInitialValue();
13}

逻辑很简单:

  • 获取当前线程内部的ThreadLocalMap
  • map存在则获取当前ThreadLocal对应的value值
  • map不存在或者找不到value值,则调用setInitialValue,进行初始化

setInitialValue()源码

 1private T setInitialValue() {
2 T value = initialValue();
3 Thread t = Thread.currentThread();
4 ThreadLocalMap map = getMap(t);
5 if (map != null)
6 map.set(this, value);
7 else
8 createMap(t, value);
9 return value;
10}

逻辑也很简单:

  • 调用initialValue方法,获取初始化值【调用者通过覆盖该方法,设置自己的初始化值】
  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

时序图

为了便于理解,笔者特地画了一个时序图,请看:

get方法时序图get方法时序图

小结

至此,您能回答ThreadLocal的实现原理了吗?没错,map,一个叫做ThreadLocalMap的map,这是关键。每一个线程都有一个私有变量,是ThreadLocalMap类型。当为线程添加ThreadLocal对象时,就是保存到这个map中,所以线程与线程间不会互相干扰。总结起来,一句话:我有我的young,哦,不对,是我有我的map。弄清楚了这些,是不是使用的时候就自信了很多。但是,这是不是就意味着可以大胆的去使用了呢?其实,不尽然,有一个“大坑”在等着你。

神奇的remove

那个“大坑”指的就是因为ThreadLocal使用不当,会引发内存泄露的问题。笔者给出两段示例代码,来说明这个问题。

代码出处来源于Stack Overflow:https://stackoverflow.com/questions/17968803/threadlocal-memory-leak

示例一:

 1public class MemoryLeak {
2
3 public static void main(String[] args) {
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 for (int i = 0; i < 1000; i++) {
8 TestClass t = new TestClass(i);
9 t.printId();
10 t = null;
11 }
12 }
13 }).start();
14 }
15
16 static class TestClass{
17 private int id;
18 private int[] arr;
19 private ThreadLocal<TestClass> threadLocal;
20 TestClass(int id){
21 this.id = id;
22 arr = new int[1000000];
23 threadLocal = new ThreadLocal<>();
24 threadLocal.set(this);
25 }
26
27 public void printId(){
28 System.out.println(threadLocal.get().id);
29 }
30 }
31}

运行结果:

 10
21
32
43
5...省略...
6440
7441
8442
9443
10444
11Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
12 at com.gentlemanqc.MemoryLeak$TestClass.<init>(MemoryLeak.java:33)
13 at com.gentlemanqc.MemoryLeak$1.run(MemoryLeak.java:16)
14 at java.lang.Thread.run(Thread.java:745)

对上述代码稍作修改,请看:

 1public class MemoryLeak {
2
3 public static void main(String[] args) {
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 for (int i = 0; i < 1000; i++) {
8 TestClass t = new TestClass(i);
9 t.printId();
10 t.threadLocal.remove();
11 }
12 }
13 }).start();
14 }
15
16 static class TestClass{
17 private int id;
18 private int[] arr;
19 private ThreadLocal<TestClass> threadLocal;
20 TestClass(int id){
21 this.id = id;
22 arr = new int[1000000];
23 threadLocal = new ThreadLocal<>();
24 threadLocal.set(this);
25 }
26
27 public void printId(){
28 System.out.println(threadLocal.get().id);
29 }
30 }
31}

运行结果:

10
21
32
43
5...省略...
6996
7997
8998
9999

一个内存泄漏,一个正常完成,对比代码只有一处不同:t = null改为了t.threadLocal.remove();哇,神奇的remove!!!笔者先留个悬念,暂且不去分析原因。我们先来看看上述示例中涉及到的两个方法:set()和remove()。

set(T value)源码

1public void set(T value) {
2 Thread t = Thread.currentThread();
3 ThreadLocalMap map = getMap(t);
4 if (map != null)
5 map.set(this, value);
6 else
7 createMap(t, value);
8}

逻辑很简单:

  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

remove源码

1public void remove() {
2 ThreadLocalMap m = getMap(Thread.currentThread());
3 if (m != null)
4 m.remove(this);
5}

就一句话,获取当前线程内部的ThreadLocalMap,存在则从map中删除这个ThreadLocal对象。

小结

讲到这里,ThreadLocal最常用的四种方法都已经说完了,细心的您是不是已经发现,每一个方法都离不开一个类,那就是ThreadLocalMap。所以,要更好的理解ThreadLocal,就有必要深入的去学习这个map。

无处不在的ThreadLocalMap

还是老规矩,先来看看类上的注释,翻译过来就是这么几点:

  • ThreadLocalMap是一个自定义的hash map,专门用来保存线程的thread local变量
  • 它的操作仅限于ThreadLocal类中,不对外暴露
  • 这个类被用在Thread类的私有变量threadLocals和inheritableThreadLocals上
  • 为了能够保存大量且存活时间较长的threadLocal实例,hash table entries采用了WeakReferences作为key的类型
  • 一旦hash table运行空间不足时,key为null的entry就会被清理掉

我们来看下类的声明信息:

 1static class ThreadLocalMap {
2
3 // hash map中的entry继承自弱引用WeakReference,指向threadLocal对象
4 // 对于key为null的entry,说明不再需要访问,会从table表中清理掉
5 // 这种entry被成为“stale entries”
6 static class Entry extends WeakReference<ThreadLocal<?>> {
7 /** The value associated with this ThreadLocal. */
8 Object value;
9
10 Entry(ThreadLocal<?> k, Object v) {
11 super(k);
12 value = v;
13 }
14 }
15
16 private static final int INITIAL_CAPACITY = 16;
17
18 private Entry[] table;
19
20 private int size = 0;
21
22 private int threshold; // Default to 0
23
24 private void setThreshold(int len) {
25 threshold = len * 2 / 3;
26 }
27
28 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
29 table = new Entry[INITIAL_CAPACITY];
30 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
31 table[i] = new Entry(firstKey, firstValue);
32 size = 1;
33 setThreshold(INITIAL_CAPACITY);
34 }
35}

当创建一个ThreadLocalMap时,实际上内部是构建了一个Entry类型的数组,初始化大小为16,阈值threshold为数组长度的2/3,Entry类型为WeakReference,有一个弱引用指向ThreadLocal对象。

为什么Entry采用WeakReference类型?

Java垃圾回收时,看一个对象需不需要回收,就是看这个对象是否可达。什么是可达,就是能不能通过引用去访问到这个对象。(当然,垃圾回收的策略远比这个复杂,这里为了便于理解,简单给大家说一下)。

jdk1.2以后,引用就被分为四种类型:强引用、弱引用、软引用和虚引用。强引用就是我们常用的Object obj = new Object(),obj就是一个强引用,指向了对象内存空间。当内存空间不足时,Java垃圾回收程序发现对象有一个强引用,宁愿抛出OutofMemory错误,也不会去回收一个强引用的内存空间。而弱引用,即WeakReference,意思就是当一个对象只有弱引用指向它时,垃圾回收器不管当前内存是否足够,都会进行回收。反过来说,这个对象是否要被垃圾回收掉,取决于是否有强引用指向。ThreadLocalMap这么做,是不想因为自己存储了ThreadLocal对象,而影响到它的垃圾回收,而是把这个主动权完全交给了调用方,一旦调用方不想使用,设置ThreadLocal对象为null,内存就可以被回收掉。

内存溢出问题解答

至此,该做的铺垫都已经完成了,此时,我们可以来看看上面那个内存泄漏的例子。示例中执行一次for循环里的代码后,对应的内存状态:

循环一次内存状态循环一次内存状态

  • t为创建TestClass对象返回的引用,临时变量,在一次for循环后就执行出栈了
  • thread为创建Thread对象返回的引用,run方法在执行过程中,暂时不会执行出栈

调用t=null后,虽然无法再通过t访问内存地址,但是当前线程依旧存活,可以通过thread指向的内存地址,访问到Thread对象,从而访问到ThreadLocalMap对象,访问到value指向的内存空间,访问到arr指向的内存空间,从而导致Java垃圾回收并不会回收int[1000000]@541这一片空间。那么随着循环多次之后,不被回收的堆空间越来越大,最后抛出java.lang.OutOfMemoryError: Java heap space。 您问:那为什么调用t.threadLocal.remove()就可以呢? 我答:这就得看remove方法里究竟做了什么了,请看:

remove时序图

是不是恍然大悟?来看下调用remove方法之后的内存状态:

remove后内存状态

因为remove方法将referent和value都被设置为null,所以ThreadLocal@540和Memory$TestClass@538对应的内存地址都变成不可达,Java垃圾回收自然就会回收这片内存,从而不会出现内存泄漏的错误。

小结

呼应文章开头提到的《Spring Cloud Netflix Zuul源码分析之请求处理篇》,其中就有一个非常重要的类:ZuulServlet,它就是典型的ThreadLocal在实际场景中的运用案例。请看:

 1public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
2 try {
3 init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
4 RequestContext context = RequestContext.getCurrentContext();
5 context.setZuulEngineRan();
6 try {
7 preRoute();
8 } catch (ZuulException e) {
9 error(e);
10 postRoute();
11 return;
12 }
13 try {
14 route();
15 } catch (ZuulException e) {
16 error(e);
17 postRoute();
18 return;
19 }
20 try {
21 postRoute();
22 } catch (ZuulException e) {
23 error(e);
24 return;
25 }
26
27 } catch (Throwable e) {
28 error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
29 } finally {
30 RequestContext.getCurrentContext().unset();
31 }
32}

您有没有发现,一次HTTP请求经由前置过滤器、路由过滤器、后置过滤器处理完成之后,都会调用一个方法,没错,就是在finally里,RequestContext.getCurrentContext().unset()。走进RequestContext一看:

1public void unset() {
2 threadLocal.remove();
3}

看到没有,神器的remove又出现了。讲到这里,您是否get到ThreadLocal正确的使用"姿势"呢?

ThreadLocalMap之番外篇

笔者之前写过关于TreeMap和HashMap的文章,凡是Map的实现,都有自己降低哈希冲突和解决哈希冲突的方法。在这里,ThreadLocalMap是如何处理的呢?请往下看。

如何降低哈希冲突

回顾ThreadLocalMap添加元素的源码:

  • 方式一:构造方法
  • 方式二:set方法

其中i就是ThreadLocal在ThreadLocalMap中存放的索引,计算方式为:key.threadLocalHashCode & (len-1)。我们先来看threadLocalHashCode是什么?

1private final int threadLocalHashCode = nextHashCode();

也就是说,每一个ThreadLocal都会根据nextHashCode生成一个int值,作为哈希值,然后根据这个哈希值&(数组长度-1),从而获取到哈希值的低N位(以len为16,16-1保证低四位都是1,从而获取哈希值本身的低四位值),从而获取到在数组中的索引位置。那它是如何降低哈希冲突的呢?玄机就在于这个nextHashCode方法。

1private static AtomicInteger nextHashCode = new AtomicInteger();
2
3private static final int HASH_INCREMENT = 0x61c88647;
4
5private static int nextHashCode() {
6 return nextHashCode.getAndAdd(HASH_INCREMENT);
7}

0x61c88647是什么?转化为十进制是1640531527。2654435769转换成int类型就是-1640531527。2654435769等于(根号5-1)/2乘以2的32次方。(根号5-1)/2是什么?是黄金分割数,近似为0.618。也就是说0x61c88647理解为一个黄金分割数乘以2的32次方。有什么好处?它可以神奇的保证nextHashCode生成的哈希值,均匀的分布在2的幂次方上,且小于2的32次方。来看例子:

 1public class ThreadLocalHashCodeTest {
2
3 private static AtomicInteger nextHashCode =
4 new AtomicInteger();
5
6 private static final int HASH_INCREMENT = 0x61c88647;
7
8 private static int nextHashCode() {
9 return nextHashCode.getAndAdd(HASH_INCREMENT);
10 }
11
12 public static void main(String[] args){
13 for (int i = 0; i < 16; i++) {
14 System.out.print(nextHashCode() & 15);
15 System.out.print(" ");
16 }
17 System.out.println();
18 for (int i = 0; i < 32; i++) {
19 System.out.print(nextHashCode() & 31);
20 System.out.print(" ");
21 }
22 System.out.println();
23 for (int i = 0; i < 64; i++) {
24 System.out.print(nextHashCode() & 63);
25 System.out.print(" ");
26 }
27 }
28}

输出结果:

10 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9
216 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9
316 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 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

看见没有,元素索引值完美的散列在数组当中,并没有出现冲突。

如何解决哈希冲突

ThreadLocalMap采用黄金分割数的方式,大大降低了哈希冲突的情况,但是这种情况还是存在的,那如果出现,它是怎么解决的呢?请看:

 1private void set(ThreadLocal<?> key, Object value) {
2
3 Entry[] tab = table;
4 int len = tab.length;
5 int i = key.threadLocalHashCode & (len-1);
6
7 // 出现哈希冲突
8 for (Entry e = tab[i];
9 e != null;
10 e = tab[i = nextIndex(i, len)]) {
11 ThreadLocal<?> k = e.get();
12
13 // 如果是同一个对象,则覆盖value值
14 if (k == key) {
15 e.value = value;
16 return;
17 }
18
19 // 如果key为null,则替换它的位置
20 if (k == null) {
21 replaceStaleEntry(key, value, i);
22 return;
23 }
24
25 // 否则往后一个位置找,直到找到空的位置
26 }
27
28 tab[i] = new Entry(key, value);
29 int sz = ++size;
30 if (!cleanSomeSlots(i, sz) && sz >= threshold)
31 rehash();
32}

当出现哈希冲突时,它的做法看是否是同一个对象或者是是否可以替换,否则往后移动一位,继续判断。

1private static int nextIndex(int i, int len) {
2 return ((i + 1 < len) ? i + 1 : 0);
3}

扩容

通过set方法里的代码,我们知道ThreadLocalMap扩容有两个前提:

  • !cleanSomeSlots(i, sz)
  • size >= threshold

元素个数大于阈值进行扩容,这个很好理解,那么还有一个前提是什么意思呢?我们来看cleanSomeSlots()做了什么:

 1private boolean cleanSomeSlots(int i, int n) {
2 boolean removed = false;
3 Entry[] tab = table;
4 int len = tab.length;
5 do {
6 i = nextIndex(i, len);
7 Entry e = tab[i];
8 if (e != null && e.get() == null) {
9 n = len;
10 removed = true;
11 i = expungeStaleEntry(i);
12 }
13 } while ( (n >>>= 1) != 0);
14 return removed;
15}

方法上注释写的很明白,从当前插入元素位置,往后扫描数组中的元素,判断是否是“stale entry”。在前面将ThreadLocalMap类声明信息的时候讲过,“stale entry”表示的是那些key为null的entry。cleanSomeSlots方法就是找到他们,调用expungeStaleEntry方法进行清理。如果找到,则返回true,否则返回false。 您问:为什么扩容要看它的返回值呢? 我答:因为一旦找到,就调用expungeStaleEntry方法进行清理。

 1private int expungeStaleEntry(int staleSlot) {
2 Entry[] tab = table;
3 int len = tab.length;
4
5 // expunge entry at staleSlot
6 tab[staleSlot].value = null;
7 tab[staleSlot] = null;
8 size--;
9
10 // 省略
11}

看到没有,size会减一,那么添加元素导致size加1,cleanSomeSlots一旦找到,则会清理一个或者多个元素,size减去的最少为1,所以返回true,自然就没有必要再判断size是否大于等于阈值了。 好了,前提条件一旦满足,则调用rehash方法,此时还未扩容:

1private void rehash() {
2 // 先清理stale entry,会导致size变化
3 expungeStaleEntries();
4
5 // 如果size大于等于3/4阈值,则扩容
6 if (size >= threshold - threshold / 4)
7 resize();
8}

哈哈,这里才是真正的扩容,要进行扩容:

  1. 当前插入元素的位置,往后没有需要清理的stale entry
  2. size大于等于阈值
  3. 清理掉stale entry之后,size大于等于3/4阈值

既然搞清楚了条件,那么满足后,又是如何扩容的呢?

 1private void resize() {
2 Entry[] oldTab = table;
3 int oldLen = oldTab.length;
4 int newLen = oldLen * 2;
5 // 新建一个数组,按照2倍长度扩容
6 Entry[] newTab = new Entry[newLen];
7 int count = 0;
8
9 for (int j = 0; j < oldLen; ++j) {
10 Entry e = oldTab[j];
11 if (e != null) {
12 ThreadLocal<?> k = e.get();
13 if (k == null) {
14 e.value = null; // Help the GC
15 } else {
16 // key不为null,重新计算索引位置
17 int h = k.threadLocalHashCode & (newLen - 1);
18 while (newTab[h] != null)
19 h = nextIndex(h, newLen);
20 // 插入新的数组中索引位置
21 newTab[h] = e;
22 count++;
23 }
24 }
25 }
26
27 // 阈值为长度的2/3
28 setThreshold(newLen);
29 size = count;
30 table = newTab;
31}

两倍长度扩容,重新计算索引,扩容的同时也顺便清理了key为null的元素,即stale entry,不再存入扩容后的数组中。

补充

不知您有没有注意到,ThreadLocalMap中出现哈希冲突时,它是线性探测,直到找到空的位置。这种效率是非常低的,那为什么Java大神们写代码时还要这么做呢?笔者认为取决于它采用的哈希算法,正因为nextHashCode(),保证了冲突出现的可能性很低。而且ThreadLocalMap在处理过程中非常注意清理"stale entry",及时释放出空余位置,从而降低了线性探测带来的低效。

文章目录
  1. 1. Hello World
  2. 2. What is this
  3. 3. 我有我的map
    1. 3.0.1. get()源码
    2. 3.0.2. setInitialValue()源码
    3. 3.0.3. 时序图
    4. 3.0.4. 小结
  • 4. 神奇的remove
    1. 4.0.1. 示例一:
    2. 4.0.2. set(T value)源码
    3. 4.0.3. remove源码
    4. 4.0.4. 小结
  • 5. 无处不在的ThreadLocalMap
    1. 5.1. 为什么Entry采用WeakReference类型?
      1. 5.1.1. 内存溢出问题解答
      2. 5.1.2. 小结
  • 6. ThreadLocalMap之番外篇
    1. 6.0.1. 如何降低哈希冲突
    2. 6.0.2. 如何解决哈希冲突
    3. 6.0.3. 扩容
    4. 6.0.4. 补充