背景
最近要做一个 ThreadLocal 的分享,把公众号,google,百度上的大多数 threadlocal 都扫了一遍,也收到公司专家的指导,于是有了这边 ThreadLocal 的文章。
历史
ThreadLocal 是解决多线程共享变量的一种方式,绝大多数主流的编程语言都支持 ThreadLocal。Java 更是在 JDK 1.2(1998 年)就支持 ThreadLocal。
引子
通过如下例子,展示 TheadLocal 是如何实现的。
public class ThreadLocalTest {
public void testThreadLocal() throws InterruptedException {
ThreadLocal<String> threadLocal1 = ThreadLocal.withInitial(() -> "000");
Thread t1 = new Thread(() -> {
threadLocal1.set("ABC");
System.out.println("thread " + Thread.currentThread() + " " + threadLocal1.get());
});
threadLocal1.set("abc");
t1.start();
t1.join();
System.out.println("thread " + Thread.currentThread() + " " + threadLocal1.get());
}
public static void main(String[] args) throws InterruptedException {
new ThreadLocalTest1().testThreadLocal();
}
}
如上图所示
1、创建了一个ThreadLocal对象threadLocal1,并初始化其值为"000"。
2、在主线程中,将threadLocal1的值设置为"abc"。
3、启动线程t1。
4、线程t1开始执行,将threadLocal1的值设置为"ABC"。
5、线程t1打印出自己的线程名和threadLocal1的值,即"ABC"。
6、主线程等待线程t1执行完毕。
7、主线程打印出自己的线程名和threadLocal1的值,即"abc"。
内存模型
如上图所示
1、每个线程包含一个 TheadLocalMap 用以保存当前线程的 ThreadLocal 对象
2、每个线程的多个 TheadLocal 对象以数组的形式保存
3、同一 TheadLocal 在不同的对象使用不同的 ThreadLocalMap
4、不同线程共享 ThreadLocal 的 threadlocalHashCode
5、ThreadLocalMap 中的每个元素为 Entry,Entry 包括 key 和 value。其中 key 为 threadlocal,value 为threadlocal对应的值
对应代码如下:
Thread
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
ThreadLocal
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
基本操作
get()
1、获取当前线程的 threadlocals
2、获取 threadlocal 对应的 value
public T get() {
// 当前所在线程
Thread t = Thread.currentThread();
// map 对应 t.threadLocals;
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();
}
set()
1、获取当前线程的 threadlocals
2、将 this、value 保存到 threadlocals
public void set(T value) {
// 当前所在线程
Thread t = Thread.currentThread();
// map 对应 t.threadLocals;
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
remove()
1、获取当前线程的 threadlocals
2、删除 this 对应的 entry
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
关键设计点
ThreadLocalMap vs HashMap
维度 | threadLocalMap | HashMap |
---|---|---|
数据结构 | 数组 | 数组 |
初始容量 | 16 | 32 |
hash 算法 | 斐波那契数 | hashCode |
hash 冲突 | 开放定址法-线性探测 | 开链法+红黑树 |
扩容条件 | 容量2/3,回收之后大于1/2容量 | 负载因子 |
扩容大小 | 2倍 | 2倍 |
hash 算法
HASH_INCREMENT 也不是随便取的,它转化为十进制是 1640531527,2654435769 转换成 int 类型就是 -1640531527,2654435769 等于 (√5-1)/2 乘以 2 的 32 次方。(√5-1)/2 就是黄金分割数,近似为 0.618,也就是说 0x61c88647 理解为一个黄金分割数乘以 2 的 32 次方,它可以保证 nextHashCode 生成的哈希值,均匀的分布在 2 的幂次方上,且小于 2 的 32 次方。
0x61c88647 是一个“魔数”,即斐波那契数,这个特定的数值被选定的原因在于它是黄金分割比(Golden Ratio)的近似整数表示。
这个常数的二进制表示形式是:0110 0001 1100 1000 1000 0110 0100 0111,可以看到,它是一个31位的数,最高位为0,这保证了当它用作增量时,计算出的 hash 值总是正数。
那么,为什么要选择黄金分割比的近似表示呢?这是因为黄金分割比有一个特性:它可以帮助实现更好的散列分布。当我们用这个数值作为增量时,即使原始的 hash 值存在规律(例如,都是偶数),经过增量计算后的 hash 值也将尽可能均匀地分布在整个 hash 表中。
此外,选择这个特定的数也是因为它与 2^32 的除数接近黄金分割比(0.6180339887),可以为 hash 值提供良好的分布。
在 ThreadLocal 类中,每个线程都有一个唯一的线程局部变量,并且每个线程局部变量都需要一个唯一的 hash 值。通过将这个“魔数”作为 hash 值的增量,可以确保每个线程局部变量的 hash 值都是独一无二的,同时也可以尽可能地减少 hash 冲突。
如下示例
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
public class HashTest {
private static final int HASH_INCREMENT = 0x61c88647;
private static int getThreadLocalHashCode(ThreadLocal<String> threadLocal) {
Field field = ReflectionUtils.findField(ThreadLocal.class, "threadLocalHashCode");
field.setAccessible(true);
return (int) ReflectionUtils.getField(field, threadLocal);
}
public static void dumpHashElements(int max) {
System.out.println("hashCode threadLocalHashCode");
for (int i = 0; i < max; i++) {
System.out.print((i * HASH_INCREMENT) & (max - 1));
System.out.print(" ");
ThreadLocal<String> threadLocal = new ThreadLocal<>();
System.out.println(getThreadLocalHashCode(threadLocal) & (max - 1));
}
System.out.println(" ");
}
public static void main(String[] args) {
dumpHashElements(16);
dumpHashElements(32);
}
}
hash 冲突解决
采用线性探测法
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);
}
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;
}
// 线性探测法
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
扩容条件是什么?如何扩容?
1、没有元素需要清理,并且容量大于 2/3,探测式容量达到阈值的 3/4
2、每次扩容为当前的 2 倍
private void rehash() {
expungeStaleEntries();
// 容量大于等于 3/4 时触发扩容
if (size >= threshold - threshold / 4)
resize();
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 容量为之前的 2 倍
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;
}
更复杂的例子
ThreadLocal 的可以是各种类型。如下例子展示了 Integer,String、Map 类型
import java.util.HashMap;
import java.util.Map;
public class ThreadLocalTest {
public static void dump(String id, ThreadLocal<Integer> threadLocal1, ThreadLocal<String> threadLocal2, ThreadLocal<Map<String, String>> threadLocal3) {
System.out.println("thread " + id + " threadlocal1 " + threadLocal1.get());
System.out.println("thread " + id + " threadlocal2 " + threadLocal2.get());
System.out.println("thread " + id + " threadlocal3 " + threadLocal3.get());
}
public static void testThreadLocal() {
final ThreadLocal threadLocal1 = ThreadLocal.withInitial(() -> 0);
final ThreadLocal<String> threadLocal2 = java.lang.ThreadLocal.withInitial(() -> "000");
final ThreadLocal<Map<String, String>> threadLocal3 = java.lang.ThreadLocal.withInitial(HashMap::new);
Thread t1 = new Thread(() -> {
threadLocal1.set(10);
threadLocal2.set("abc");
dump("t1", threadLocal1, threadLocal2, threadLocal3);
});
Thread t2 = new Thread(() -> {
threadLocal1.set(20);
threadLocal2.set("ABC");
dump("t1", threadLocal1, threadLocal2, threadLocal3);
});
Thread t3 = new Thread(() -> {
threadLocal1.set(30);
Map<String, String> val = new HashMap<>();
val.put("123", "XXX");
threadLocal3.set(val);
dump("t1", threadLocal1, threadLocal2, threadLocal3);
});
t1.start();
t2.start();
t3.start();
}
public static void main(String[] args) {
testThreadLocal();
}
}
输出
thread t2 threadlocal1 20
thread t2 threadlocal2 ABC
thread t1 threadlocal1 10
thread t3 threadlocal1 30
thread t3 threadlocal2 000
thread t1 threadlocal2 abc
thread t2 threadlocal3 {}
thread t1 threadlocal3 {}
thread t3 threadlocal3 {123=XXX}
应用场景
条件:每个线程 threadlocal 数量笔记少
1、全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
2、Spring 事务管理器采用了 ThreadLocal
3、Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
疑问
1、ThreadLocalMap 的 Entry 为什么继承自 WeakReference?
2、每个线程 threadlocal 的数量很多会有什么问题?如何处理? 速度变慢(线性探测法,数据清理)
3、如果当前线程创建子线程,当前线程的 ThreadLocal 数据可以传递给子线程的 ThreadLocal 么?为什么?
4、thread 如果退出,threadlocalMap 可以回收么?
5、thread 长时间运行(比如线程池),threadlocalMap 中的值没有引用了,如何处理,不处理会有什么问题?
6、theadlocal 中 Entry 中为什么需要 key 为 threadlocal?
内存泄漏
ThreadLocalMap 的 Entry 为什么继承自 WeakReference?
假设为强引用,threadlocal 不再使用,但是 TheadLocalMap 到 Entry,Entry 到 key 的强引用一直存在,那么,就导致内存泄漏。
public class ThreadLocalDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(() -> test("abc", false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}
private static void test(String input, boolean isGC) {
try {
new ThreadLocal<>().set(input);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(t);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们应用中,线程常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着!
启发式清理
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
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;
}
最后
赶紧打开你们项目的代码,看看 ThreadLocal 的使用是不是有问题