什么是ThreadLocal?
ThreadLocal是Java中一个非常重要的线程封闭工具,它用于创建线程局部变量。每个线程都有自己独立初始化的变量副本,从而避免了多线程环境下的共享和同步问题。
ThreadLocal实现原理
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);
}
}
从这个方法能够看出,set方法是根据当前线程对象去取出一个ThreadLocalMap对象,实际的数据是放在线程中的ThreadLocalMap对象中的。键是ThreadLocal对象,值是传入的参数。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从getMap这个方法可以看出,这个ThreadLocalMap对象是线程对象的一个数据对象。
Thread类中的数据域:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在默认情况下,线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal中的set或者get方法时才会创建它们,因此在ThreadLocal中的set方法中会判断map是否存在,如果不存在,就会调用createMap来创建。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get方法
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();
}
get方法依然是先获取线程中的ThreadLocalMap,然后以ThreadLocal对象为键,查找对应的值。
setInitialValue是一个兜底操作,放入一个初始化的值,并返回,默认是返回null,可以重写返回一个实际的值。
下面举一个实际例子:
public class ThreadLocalWithDefault<T> extends ThreadLocal<T> {
public interface DefaultObjectFactory<T>{
public T getDefault();
}
private DefaultObjectFactory<T> defaultObjectFactory;
public ThreadLocalWithDefault(DefaultObjectFactory<T> defaultObjectFactory) {
this.defaultObjectFactory=defaultObjectFactory;
}
@Override
protected T initialValue() {
return defaultObjectFactory.getDefault();
}
}
public static void main(String[] args) {
ThreadLocalWithDefault<String> threadLocalWithDefault=new ThreadLocalWithDefault<String>(()->{
return "hello,Thread";
});
String s = threadLocalWithDefault.get();
System.out.println(s);
}
这个ThreadLocalWithDefault重写了initialValue,因此当线程直接get之后,可以获得到initialValue返回的值,并将其设置在ThreadLocal中。
数据清理
从上面可以看出,数据实际上是存在线程中的,当线程不退出的情况下,对象的引用将一致存在。
当线程退出的时候,会做一些清理操作,其中包括清理ThreadLocalMap。
private void exit() {
if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
TerminatingThreadLocal.threadTerminated();
}
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
因此,使用线程池的时候,当前线程会保留在线程池中,如果将一个比较大的对象设置在ThreadLocal中,可能会出现内存泄漏。
因此要及时回收对象,使用ThreadLocal对象的remove方法。
ThreadLocalMap
ThreadLocalMap中的Entry实现了弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
而且,值得注意的是,这个Entry与一般的Entry不同,ThreadLocalMap中传入的键成为了一个弱引用对象。
为什么要将键设置成为一个弱引用对象?
这是为了避免内存泄漏,防止ThreadLocal对象本身的内存泄漏。
因为ThreadLocalMap是在线程中的,如果不使用弱引用,当外部的ThreadLocal强引用消失后,Entry这里还在强引用ThreadLocal对象,而且仅有Entry还在强引用这个ThreadLocal对象,而我们此时无法将这个ThreadLocal删除了,这个导致这个对象无法被GC。因此设计为弱引用,解决了键的泄漏问题。
内存泄漏
虽然官方将键设置为弱引用,为我们解决了键的泄漏问题,但是ThreadLocal依然会出现内存泄漏,其根本原因在于ThreadLocalMap中的Entry中键是弱引用,而值是强引用。这里的泄漏是值泄漏。
在正常情况下:
ThreadLocal tl = new ThreadLocal();创建一个tl对象。- 线程通过
tl.set(value)存储数据。 - 当
tl不再被需要时,将tl变量置为 null(tl = null;)。 - 由于 Entry 的 Key 是弱引用,在下次 GC 时,这个 Key 会被回收,Entry 的 Key 变为
null。 - 当下次操作 ThreadLocalMap(如 set、get、remove)时,Map 会自动清理这些
key==null的条目(惰性清理)。 - 清理后,Value 的强引用断开,可以被正常回收。
在泄漏的情况(常见于线程池)::
- 强引用消失:
tl变量被置为 null,堆中的 ThreadLocal 对象只被 Entry 的 Key 弱引用着。 - Key 被回收:GC 发生时,由于弱引用的特性,堆中的 ThreadLocal 对象被回收。Entry 中的 Key 变为
null,但 Value 依然被 Entry 强引用。 - 线程未终止:如果这个线程是线程池中的核心线程,它会一直存活(与线程池同生命周期),那么它的
ThreadLocalMap会一直存在。 - Value 无法释放:这个
key=null的 Entry 及其 Value 对象,由于线程的强引用链(Thread -> ThreadLocalMap -> Entry -> Value)一直存在,只要线程存活,即使你再也无法通过任何代码访问到这个 Value(因为 Key 没了,get不到),它也无法被回收。这就造成了内存泄漏。 - 更糟的情况:如果这个 Value 本身又间接引用了其他大对象,会导致一连串的对象都无法被回收。
泄漏主要是ThreadLocalMap的惰性清理没有触发,导致Entry中的value没有释放。
ThreadLocal不支持继承
下面以一个例子来展示:
public static void main(String[] args) {
//当前父线程设置
ThreadLocal<String> threadLocal=new ThreadLocal<String>();
threadLocal.set("parent");
Thread thread=new Thread(()->{
System.out.println("子线程:"+threadLocal.get());
});
thread.start();
System.out.println("父线程:"+threadLocal.get());
}
结果如下:
子线程:null
父线程:parent
同一个ThreadLocal变量在父线程中被设置值后, 在子线程中是获取不到的。因为main线程和它的子线程不是同一个线程,因此是获取不到的。
但是有些场景下,我们需要父线程设置了之后,在父线程中开启的任何子线程都可以继承父线程设置的变量。
InheritableThreadLocal类
InheritableThreadLocal就是为了解决上述问题而产生的:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
public InheritableThreadLocal() {}
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);
}
}
InheritableThreadLocal重写了createMap,现在第一次调用set方法的时候,创建的是当前线程的inheritableThreadLocals变量的实例,而不是threadLocals。
与此同时,也重写了getMap方法,现在返回的是线程的inheritableThreadLocals而不是threadLocals。
访问父线程变量的方法
回到Thread的构造方法中:
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
//获取创建该线程的当前线程,也就是要创建线程的父线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(
SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
//当要启用继承ThreadLocal且父线程的inheritableThreadLocals不为空
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
//为创建的线程设置inhertiableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
this.tid = nextThreadID();
}
因此主要的实现在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 (Entry e : parentTable) {
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++;
}
}
}
}
这个ThreadLocalMap的私有构造方法:
-
继承父线程的 ThreadLocal 值
-
在创建子线程时,将父线程的
ThreadLocalMap中的数据浅拷贝到子线程 -
主要用于实现
InheritableThreadLocal功能
下面关注一下childValue方法,它的功能是传入一个父线程的参数parentValue,然后返回一个子线程的参数childValue。其默认实现是:
protected T childValue(T parentValue) {
return parentValue;
}
这个默认实现是将父线程的参数原封不动的返回作为子线程的参数。
因此上述的步骤表明,之所以子线程能够访问父线程的数据,是因为将父线程的数据浅拷贝了下来。
使用inheritableThreadLocal的示例
public static void main(String[] args) {
//当前父线程设置
ThreadLocal<String> threadLocal=new InheritableThreadLocal<String>();
threadLocal.set("parent");
Thread thread=new Thread(()->{
System.out.println("子线程:"+threadLocal.get());
});
thread.start();
System.out.println("父线程:"+threadLocal.get());
}
输出的结果:
子线程:parent
父线程:parent
值得注意的是,数据浅拷贝的操作仅仅会发生在构造线程的时候。因此
public static void main(String[] args) {
//当前父线程设置
ThreadLocal<String> threadLocal=new InheritableThreadLocal<String>();
Thread thread=new Thread(()->{
System.out.println("子线程:"+threadLocal.get());
});
threadLocal.set("parent");
thread.start();
System.out.println("父线程:"+threadLocal.get());
}
如果在构造线程之后,再设置InheritableThreadLocal中的值,那么子线程中无法获取到这个值。
父线程:parent
子线程:null
同样,如果在线程创建之后,更新父线程中的值,子线程也无法无法获取到最新的值。
public static void main(String[] args) {
//当前父线程设置
ThreadLocal<String> threadLocal=new InheritableThreadLocal<String>();
threadLocal.set("parent");
Thread thread=new Thread(()->{
System.out.println("子线程:"+threadLocal.get());
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("子线程:"+threadLocal.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.start();
System.out.println("父线程:"+threadLocal.get());
threadLocal.set("parent-new");
System.out.println("父线程:"+threadLocal.get());
}
结果:
父线程:parent
子线程:parent
父线程:parent-new
子线程:parent