「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
前言:
接上文Java实战指南|玩转接口验签-你和高手只差俩个自定义注解 我们介绍了接口验签的时候,共享线程资源所使用的线程局部变量ThreadLocal
,我们在使用的时候强调了,在拦截器的postHandle方法里一定要写ThreadContextHolder.destroy();
也就是ThreadLocal.remove()
,否则的话可能会造成内存泄漏,今天我们来分析一下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提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值;
ThreadLocal可以解释成线程的局部变量,也就是说一个ThreadLocal的变量只有当前自身线程可以访问,别的线程都访问不了,因此,ThreadLocal就避免了线程竞争,我们也可以说他是线程安全的,ThreadLocal从根源上就避免了线程冲突的发生。
基本使用
创建一个userId变量,由于ThreadLocal是一个泛型类,这里指定了userId的类型为整数。
private ThreadLocal<Integer> userId = new ThreadLocal<>();
复制代码
设置/获取 ThreadLocal的值,使用的时候我们可以把它想象成一个Map
public static void setUserId(Integer uid) {
userId.set(uid);
}
public static Integer getUserId() {
return userId.get();
}
复制代码
withInitial()方法:初始化ThreadLocal的值(所有线程可见)
private ThreadLocal<Integer> userId = ThreadLocal.withInitial(() -> 1001);
复制代码
源码解析
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
//获得当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap,
ThreadLocalMap map = getMap(t);
if (map != null) {
//通过当前实例获取ThreadLocalMap的变量,
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//获取ThreadLocal变量的value
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//初始化ThreadLocalMap
return setInitialValue();
}
复制代码
可以看到,ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。ThreadLocal.ThreadLocalMap threadLocals = null;
我们看一下ThreadLocalMap.Entry的源码:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
复制代码
由上边代码我们可以发现Entry的key是一个弱应用,如果这个变量不再被其他对象使用时,自动回收这个ThreadLocal对象,避免可能的内存泄露,但是,Entry中的value,依然是强引用,不会被回收掉;
ThreadLocal内存泄漏原理分析:
上边我们说了ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用,那什么时候value在什么时候才会被回收呢?
由上边我们查看的源码我们可以看一下 value的引用链条:value->Entry->ThreadLocalMap->Thread
;
由此可见,只要线程不销毁,我们的value就不会被回收掉,当然我们大部分的情况下 线程都是用完就销毁的,什么情况下不会销毁线程呢?相信很多人都有答案了,那就是线程池的常驻核心线程他会活跃在整个系统的生命周期中;
remove():
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
复制代码
其实ThreadLocal在我们去get的时候如果你get的key没有找到的话 他会自动清理ThreadLocalMap的,但是我们通常的使用基本上都是访问那几个不变的key,比我们上一篇文章我们每次请求都是查的userId;
可以看一下下边的源码,找不到key则自动清理getEntryAfterMiss
;
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
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);
}
/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
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;
}
复制代码
set()方法和remove()方法都会调用到expungeStaleEntry()
,进行value的清理;但是当你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生;
所以,当你不需要这个ThreadLocal变量时,主动调用remove()
ok!本期内容到此结束,你学废了吗,希望可以对大家有帮助,有不对的地方希望大家可以提出来的,共同成长;
整洁成就卓越代码,细节之中只有天地