ThreadLocal
思维导图
概述
ThreadLocal(线程局部变量),作用是保存每个线程的私有变量,以空间换时间的方式,为每一个线程保存一份私有变量,也就不存在所谓的并发问题。
真实的数据并不会存在 ThreadLocal 中。
实际上,数据都保存在 Thread 对象中 Thread#threadLocals 这个成员变量里,所以一定程度上 ThreadLocal 只是一个操作该集合的工具类。
以下就是 ThreadLocalMap 在Thread中的变量声明:
threadLocals 是给 ThreadLocal 用的,该类只能访问当前线程中的数据。
inheritableThreadLocal 是给 InheritableThreadLocal 用的,使用该类子线程可以访问到父线程的数据。
ThreadLocal 的相关操作
ThreadLocal
的内部方法因为逻辑都不复杂,不需要单独出来看,就直接全放一块了.
数据获取 - Get
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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(); }
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } protected T initialValue() { return null; }
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
|
整个获取的过程其实并不难:
- 通过 Thread#currentThread 方法获取当前线程对象。
- 首先通过 getMap 方法获取当前线程绑定的 threadLocals。
- 不要为空时,以当前 ThreadLocal 对象为参数获取对应的Entry 对象,为空跳到第四步。
- 获取 Entry 对象中的 value ,并返回。
- 调用 setInitialValue方法,并返回。
这里可以很明显的看出来,数据其实还是保存在 Thread 对象里的。
通过 setInitialValue 方法可以设定初始值。
例如,希望统计每个线程的某个操作计数,那么就可以用如下的方法:
| ThreadLocal<Integer> counter = new ThreadLocal<Integer>() { @Override protected Integer initialValue() { return 0; } };
|
以 0 为初始值做统计。
数据存储 - 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 对象。
- map 不为空时,直接set。
- map 为空时需要先创建 Map 并赋值。
ThreadLocalMap
ThreadLocalMap 类似于 HashMap ,也是使用 Hash 算法定位存取的数据结构,以 ThreadLocal 对象为 Key。
Hash 算法合理时 ThreadLocalMap 的存取操作近乎是 O(1) 的复杂度。
ThreadLocalMap
出人意料的并没有继承任何一个类或接口,是完全独立的类,以为会像 HashMap 一样继承一下 AbstractMap。
成员变量
| private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
|
ThreadLocalMap 的底层数据结构是 Entry 的数组,并且默认容量为16。
以下为 Entry 对象的声明形式:
WeakReference 声明了 Entry 对象对于 Key ,也就是 ThreadLocal 对象的引用是弱引用。
弱引用消除了 ThreadLocalMap 的引用对 ThreadLocal 的对象回收的影响,这是 ThreadLocal 避免内存泄漏的核心。
元素获取
getEntry(ThreadLocal<?> key)
- 该方法就是通过 ThreadLocal 对象获取对应的数据。
| private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
|
起手就是一个 HashCode & (len - 1),和 HashMap 类似,但ThreadLocal 的 HashCode 和 HashMap 中的直接调用 hashCode() 方法不同。
ThreadLocal 是采用递增的形式,而非直接计算对象的 HashCode。
| private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
|
以上就是 HashCode 的获取方式,是以类变量的方式递增获取,相对于直接调用 hashCode() 可以更好的减少 hash 冲突。
每次创建一个 ThreadLocal,hashCode 都会+1,所以能使数据更加均匀的散布在数组中,更好的减少 hash 冲突。
如果hash计算出来的下标存在想要的元素就直接返回,如果获取元素为空还会再调用 getEntryAfterMiss
做冲突查询的后续处理.
getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
- 该方法是在直接按照
Hash
计算下标后,没获取到对应的 Entry
对象的时候调用,下标处不是想要的元素就说明出现了 Hash 冲突。
以下为方法源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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; }
|
在判断出现 hash 冲突之后,直接往后线性查找之后的数组元素。
expungeStaleEntry(int staleSlot)
- 该方法用来清除
staleSlot
位置的 Entry 对象,并且会清理当前节点到下一个 null
节点中间的过期 Entry
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null; tab[staleSlot] = null; size--; 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; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
|
该方法是对内存泄露的进一步处理。
如果将ThreadLocal的内存泄露问题分成两个部分来看,一个是 Key,另外一个就是 Value。
Key 的部分依靠弱引用清除,如果外部的强引用断开之后,也就是没有地方在使用到该 Key 之后,Key 会被 GC 回收,所以引用就为 null。
从而判断 Key 为 null 的 Value 就是 Stale 的对象,则靠该方法清除。
ThreadLocal 靠弱引用清除的只有 Key 对象,还有 Value 对象则需要靠扫描,所以内存泄露的情况并不是能够完全避免的。
元素添加
set(ThreadLocal<?> key, Object value)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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(); }
|
通过 hashCode 确定下标后,如果 Key 相等则直接覆盖原数据,如果 Key 不相等则往后线性查找元素,找到为 null 的元素直接覆盖,或者找到空余的位置赋值。
最后会清理旧的元素,并且判断 threshold,决定是否需要扩容。
ThreadLocalMap 处理 Hash 冲突的方法叫做 线性寻址法,在冲突之后往后搜索,找到第一个为空的下标并保存元素。
线性寻址法在出现 Hash 冲突的时候处理的复杂度基本会变成 O(n),并不能直接找一个 null 点就存储,因为数组中可能还有相同的 Key 在后面。
replaceStaleEntry
- 源码中只有从上面
1.
处进入该方法,用于替换 key
为空的 Entry
节点,顺带清除数组中的过期节点.
往后搜索的是第一个为空或者 Key 相等,如果先找到 Key 为空的并不能保证后续的节点没有 Key 相等的,所以在 replaceStaleEntry 方法中可能还需要处理另外一个 Key 相同的节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; 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); }
|
如上所说,再出现 Hash 冲突的时候,往后搜索的是第一个为空的节点,并不能直接赋值,因为在后续的数组中可能还存在相同的 Key 的节点。
替换元素之前会先向前搜索找到一个 Key 为 null 的节点。
cleanSomeSlots
- 该方法的功能是就是清除数组中的过期
Entry
- 首次清除从
i
向后开始遍历log2(n)
次,如果之间发现过期Entry
会直接将n
扩充到len
可以说全数组范围的遍历.发现过期Entry
就调用expungeStaleEntry
清除直到未发现Entry
为止.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
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; }
|
扩容调整方法
rehash
- 容量调整的先驱方法,先清理过期
Entry
,并做是否需要resize
的判断
- 调整的条件是当前size大于阈值的3/4就进行扩容
| private void rehash() { expungeStaleEntries(); if (size >= threshold - threshold / 4) resize(); }
|
resize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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; } 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; }
|
Q&A
Q: ThreadLocal 为何会出现内存泄露?
ThreadLocal 会出现内存泄露的主要原因是如果是强引用,那么在 ThreadLocal 类不再使用之后,ThreadLocalMap 中无法清除相关的 Entry 对象。
在 ThreadLocal 不再使用之后,ThreadLocalMap 中指向 ThreadLocal 的强引用也会导致 ThreadLocal 无法被 GC 回收,同理 Value 对象也被保留了下来。
也就出现了所谓的内存泄露,无用的数据无法被 GC 有效的清除。
Q: ThreadLocal 如何解决内存泄漏?
ThreadLocal 的内存泄露可以分为 Key(也就是 ThreadLocal),以及 Value。
解决 Key 的内存泄露的方法就是采用弱引用,弱引用消除了 ThreadLocalMap 对 ThreadLocal 对象的 GC 的影响。
另外的在每次获取或者添加数据的时候都会判断 Key 是否被回收,如果 Key 已经被回收会连带清理 Value 对象,这也就顺带解决了 Value 的泄露问题。
Q: ThreadLocalMap 如何解决Hash冲突?
Hash 冲突就是指通过 Hash 计算的下标值一致,两个元素的定位一致。
HashMap 解决 Hash 冲突的方法就是拉链法,底层的数组中保存的不是单一的数据,而是一个集合(链表/红黑树),冲突之后下挂。
采用拉链法的结果就是在Hash冲突严重时会严重影响时间复杂度,因为就算是红黑树查询的事件复杂度都是 O(Log2n)。
ThreadLocalMap 并没有采用这种方法,而是使用的开放寻址法,如果已经有数据存在冲突点,就在数组中往下遍历找到第一个空着的位置。
需要注意的是,并不是找到空的位置就可以直接替换,还是需要遍历整个数组确保没有重复的 Key。
Q: ThreadLocalMap 和 HashMap 的异同
两个都是采用 Hash 定位的数据结构,底层都是以数组的形式。
但是 HashCode 的获取方式不同,HashMap 调用对象的 hashCode() 方法,而 ThreadLocalMap 中的 Key 就是 ThreadLocal,ThreadLocal 的 HashCode 是递增分配的。
另外处理 Hash 冲突的方式不同,ThreadLocalMap 采用的开放寻址法,而 HashMap 采用的是拉链法。