ThreadLocal 的实现机制与踩坑
ThreadLocal简介
ThreadLocal主要提供thread-local变量(线程本地变量),与共享变量不同,ThreadLocal 让每个线程都将目标数据复制一份作为线程私有,后续对于该数据的操作都是在各自私有的副本上进行,线程之间彼此相互隔离,也就不存在竞争问题。
访问变量的时候我们可以通过get/set方法访问。ThreadLocal变量一般是私有static类型,与线程状态紧密联系,比如绑定在线程的事务id或者用户数据。可以通过如下方式访问:
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
类图

通过类图我们可以看到:
- ThreadLocal类目依赖ThreadLocalMap数据结构存储数据
- ThreadLocalMap类目是一个自定义的Entry对象
- ThreadLocalMap中key是ThreadLocal自身,value是ThreadLocal绑定的数据
ThreadLocal源码分析
set(T)方法
public void set(T value) {
//1. 获取当前线程
Thread t = Thread.currentThread();
//2. 获取当前线程绑定的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//3.1 设置当前线程和value到ThreadLocalMap
map.set(this, value);
else
//3.2 如果没有ThreadLocalMap,则初始化
createMap(t, value);
}
getMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
createMap
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
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);
}
ThreadLocalMap初始化包含两个参数,当前线程和当前线程的绑定value
特别需要注意的是,对应的的数据是在数组链表中Entry中的下表i,而下标计算如下:
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
//这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
从下标计算可以看到,同一个线程重复生成ThreadLocal对象,对应的HashCode值是不一样的,进而确认唯一的ThreadLocal。
0x61c88647
在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。
这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。
斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647 。
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。
map.set
如果已经对应的ThreadLocal已经存在数据,那我们需要将当前线程和value绑定存入ThreadLocalMap。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//1. 计算下标i
int i = key.threadLocalHashCode & (len-1);
//2. 获取i位置的Entry对象,如果发现hash冲突,则调用nextIndex
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//3. 获取Entry对象绑定的ThreadLocal
ThreadLocal<?> k = e.get();
//4. 如果绑定对象与设置对象相同,即是同一个线程设置,则直接替换value
if (k == key) {
e.value = value;
return;
}
//5. 如果k为null,则替换当前k v
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//清理k为null的数据
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里我们可以看到每次set方法之后都会做清理工具,所以jdk推荐ThreadLocal变量是private static的,这样子就可以一直持有ThreadLocal对象一直清理,避免内存泄露。
所以 出现内存泄露的前提必须是持有 value 的线程一直存活,这在使用线程池时是很正常的,在这种情况下 value 一直不会被 GC,因为线程对象与 value 之间维护的是强引用。此外就是 后续线程执行的业务一直没有调用 ThreadLocal 的 get 或 set 方法,导致不会主动去删除 key 为 null 的 value 对象,在满足这两个条件下 value 对象一直常驻内存,所以存在内存泄露的可能性。
比如我们的Web Server其实也可以看做一个大型线程池,这一点尤其要注意。
get()
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
/获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据当前线程 获取Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//记录初始值
return setInitialValue();
}
remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal问题
基本上遇到过三类ThreadLocal问题,或者说我们比较容易踩坑:
- 使用先get后set模式导致绑定变量出现问题(一般在Web应用中,比较常见到)
- 没有调用remove方法导致的内存泄露问题
- ThreadLocal修饰共享变量,导致共享变量数据访问被共享
数据访问问题
这个数据我们自己的代码bug,尤其是web应用中我们通常使用ThreadLocal来方便的传递会话对象。
但是曾经有一次我们就发现线上的会话出现了问题,A用户访问到了B用户的数据,通过查看代码:
//获取ThreadLocal绑定对象
HttpContext context = HttpContext.getContext();
if (context != null && context.getUser() != null) {
// 处理业务逻辑
}else{
context.setUser(new User());
}
如上代码,每一次http请求,都是先判断是否存在,如果存在则做业务处理,不存在则设置,而WebServer的线程会共享,导致部分用户可能会获取之前用户已经设置的变量。
public class ThreadLocalWithThreadPool implements Callable<Boolean> {
private static final int N_CPU = 4;
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> {
System.out.println("thread-" + Thread.currentThread().getId() + " init thread local");
return "";
});
public static void main(String[] args) throws Exception {
System.out.println("cpu core size : " + N_CPU);
List<Callable<Boolean>> tasks = new ArrayList<>(N_CPU * 2);
ThreadLocalWithThreadPool tl = new ThreadLocalWithThreadPool();
for (int i = 0; i < N_CPU * 2; i++) {
tasks.add(tl);
}
ExecutorService es = Executors.newFixedThreadPool(2);
List<Future<Boolean>> futures = es.invokeAll(tasks);
for (final Future<Boolean> future : futures) {
future.get();
}
es.shutdown();
}
@Override
public Boolean call() {
String li = threadLocal.get();
if (StringUtils.isNotEmpty(li)) {
System.out.println(Thread.currentThread().getId() + "_get_" + li);
} else {
li = Thread.currentThread().getId() + "_" + RandomUtils.nextInt(10);
System.out.println(Thread.currentThread().getId() + "_set_" + li);
threadLocal.set(li);
}
return true;
}
}
如上代码,我们会发现线程获取的变量一直是一样的。
这种就完全属于对ThreadLocal机制了解的问题了,需要提高个人编码水平
未调用remove方法导致的内存溢出问题
public class ThreadLocalWithMemoryLeak implements Callable<Boolean> {
private static final int N_CPU = 4;
private class My50MB {
private byte[] buffer = new byte[50 * 1024 * 1024];
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("gc my 50 mb");
}
}
private ThreadLocal<My50MB> threadLocal = new ThreadLocal<>();
@Override
public Boolean call() {
System.out.println("Thread-" + Thread.currentThread().getId() + " is running");
threadLocal.set(new My50MB());
return true;
}
public static void main(String[] args) throws Exception {
TimeUnit.SECONDS.sleep(20);
List<Callable<Boolean>> tasks = new ArrayList<>(N_CPU * 2);
ThreadLocalWithMemoryLeak tl = new ThreadLocalWithMemoryLeak();
for (int i = 0; i < N_CPU * 2; i++) {
tasks.add(tl);
}
ExecutorService es = Executors.newFixedThreadPool(N_CPU * 2);
List<Future<Boolean>> futures = es.invokeAll(tasks);
for (final Future<Boolean> future : futures) {
future.get();
System.gc();
}
TimeUnit.SECONDS.sleep(30);
es.shutdown();
System.out.println("shutdown");
TimeUnit.SECONDS.sleep(60);
}
}

我们发现,没有调用remove方法,在整个县城的生命周期中,内存都没有释放,那我们加上remove方法呢?
@Override
public Boolean call() {
try {
System.out.println("Thread-" + Thread.currentThread().getId() + " is running");
threadLocal.set(new My50MB());
return true;
}finally {
threadLocal.remove();
}
}
发现内存曲线也差不多,这里还需要继续理解,只能说gc还是可以发现这些弱引用的。
所以 出现内存泄露的前提必须是持有 value 的线程一直存活,这在使用线程池时是很正常的,在这种情况下 value 一直不会被 GC,因为线程对象与 value 之间维护的是强引用。此外就是 后续线程执行的业务一直没有调用 ThreadLocal 的 get 或 set 方法,导致不会主动去删除 key 为 null 的 value 对象,在满足这两个条件下 value 对象一直常驻内存,所以存在内存泄露的可能性。
那么我们应该怎么避免呢?前面我们分析过线程池情况下使用 ThreadLocal 存在小地雷,这里的内存泄露一般也都是发生在线程池的情况下,所以在使用 ThreadLocal 时,对于不再有效的 value 主动调用一下 remove 方法来进行清除,从而消除隐患,这也算是最佳实践吧。
ThreadLocal修饰共享变量
共享变量哪怕被ThreadLocal修饰,但是因为访问的还是共享变量,最终还是会出现访问问题。
InheritableThreadLocal
ThreadLocal无法满足父子线程之间的数据传递,于是就有了InheritableThreadLocal。
/**
* InheritableThreadLocal 测试子线程与父线程之间传递数据
*/
public class InheritableThreadLocalTest {
private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
public void setRequestId(Integer requestId) {
requestIdThreadLocal.set(requestId);
doBusiness();
}
private void doBusiness() {
System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
(new Thread(() -> {
System.out.println("子线程启动");
System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
})).start();
}
public static void main(String[] args) {
Integer reqId = 5;
InheritableThreadLocalTest a = new InheritableThreadLocalTest();
a.setRequestId(reqId);
}
}
基于Thread对象,包含2个属性
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
其中inheritableThreadLocals,用来实现父子线程传递问题,我们看InheritableThreadLocal源码也比较简单
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
唯一重写的就是getMap和createMap方法,其他继承自ThreadLocal
在Thread#init方法中,实现对inheritableThreadLocals的初始化
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
如上代码,我们可以看到本质上就是一个拷贝父线程中 ThreadLocal 变量值的过程。
父线程和子线程在 ThreadLocal 变量的存储上仍然是隔离的,只是在初始化子线程时会拷贝父线程的 ThreadLocal 变量,之后在运行期间彼此互不干涉,也就是说在子线程启动起来之后,父线程和子线程各自对同一个 InheritableThreadLocal 实例的改动并不会被对方所看见。
FastThreadLocal
Netty重新设计了更快的FastThreadLocal,主要实现涉及FastThreadLocalThread、FastThreadLocal和InternalThreadLocalMap类,FastThreadLocalThread是Thread类的简单扩展,主要是为了扩展threadLocalMap属性。
类图如下

public class FastThreadLocal<V> {
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
static final AtomicInteger nextIndex = new AtomicInteger();
nextIndex是InternalThreadLocalMap父类的一个全局静态的AtomicInteger类型的对象,这意味着所有的FastThreadLocal实例将共同依赖这个指针来生成唯一的索引,而且是线程安全的。
最终set方法,我们可以看到底层实现:
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
最终避免了hash冲突的计算等操作。
引用
https://my.oschina.net/wangzhenchao/blog/3212438
https://blog.csdn.net/nmgrd/article/details/88131869
https://my.oschina.net/andylucc/blog/614359
1450

被折叠的 条评论
为什么被折叠?



