ThreadLocal是什么
ThreadLocal 提供了线程私有的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。当一个线程结束时,所有 ThreadLocal 中标记为该线程所拥有的实例副本都会被回收。
应用场景
ThreadLocal 适用于每个线程需要自己独立的实例,且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
-
线程间隔离
-
方法间共享
使用示例
一个最简单的使用示例如下:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();
public void setThreadLocal(String value) {
threadLocalString.set(value);
}
public String getThreadLocal() {
return threadLocalString.get();
}
public static void main(String[] args) {
final ThreadLocalTest test = new ThreadLocalTest();
new Thread(new Runnable() {
@Override
public void run() {
String s1 = "s1";
test.setThreadLocal(s1);
try {
Thread.sleep(1000);//加一秒睡眠是为了让下面那个线程先把赋值操作做完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.getThreadLocal());
}
},"t1").start();
new Thread(new Runnable() {
@Override
public void run() {
String s2 = "s2";
test.setThreadLocal(s2);
System.out.println(test.getThreadLocal());
}
},"t2").start();
}
}
打印结果:
s2
s1
ThreadLocal在JDK8中的实现
静态内部类Entry
ThreadLocal 类,ThreadLocalMap类,Entry类之间的关系:
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
}
}
}
ThreadLocal 类的静态内部类 ThreadLocalMap的实例会维护某个 ThreadLocal 与具体实例的映射。
与我们平常理解的map不同的是,ThreadLocalMap 的每个 Entry 都是一个对键(就是下文代码中的ThreadLocal<?> k
)的弱引用。
另外,每个 Entry 都包含对值(Object v
)的强引用。
什么叫弱引用、强引用?传送门:一文读懂Java四种引用类型
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
使用弱引用的原因在于,当没有强引用指向ThreadLocal<?> k
时,它可被回收,从而避免它不能被回收而造成的内存泄漏的问题。
但是,这里又可能出现另外一种内存泄漏的问题。
ThreadLocalMap 维护ThreadLocal 变量与具体实例(ThreadLocal<?> k
和Object v
)的映射关系,当 ThreadLocal<?> k
被回收后,这个映射关系的键变为 null,但 Entry 无法被移除。
Entiry没有被移除意味着什么?就是实例Object v
被该 Entry 引用而无法被回收造成内存泄漏。(上文代码中可以看到Object value
是Entiry类下的一个属性,所以自然有Entry引用Object value
)
那么这个问题是如何解决的?别着急,我们一步一步看。
读取实例
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
读取实例时,线程首先通过getMap(t)方法获取自己的 ThreadLocalMap。
从下文中getMap(Thread t)
方法的定义可见,该 ThreadLocalMap 的实例是 Thread 类的一个字段threadLocals
。
也就是由 Thread 维护 ThreadLocal 对象与具体实例的映射。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
获取到 ThreadLocalMap 后,通过上文代码中的map.getEntry(this)
方法获取该 ThreadLocal 在当前线程的 ThreadLocalMap 中对应的 Entry。
该方法中的 this 即当前访问的 ThreadLocal 对象。
如果获取到的 Entry 不为 null,从 Entry 中取出值即为所需访问的本线程对应的实例。
如果获取到的 Entry 为 null,则通过setInitialValue()
方法设置该 ThreadLocal 变量在该线程中对应的具体实例的初始值。
设置初始值
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;
}
首先,通过initialValue()
方法获取初始值。该方法为 public 方法,且默认返回 null。所以典型用法中常常重载该方法。上例中即在内部匿名类中将其重载。
然后通过getMap(t)
拿到该线程对应的 ThreadLocalMap 对象。
若该对象不为 null,则直接将该 ThreadLocal 对象this
与对应实例初始值value
的映射添加进该线程的 ThreadLocalMap中。
若为 null,则先创建该 ThreadLocalMap 对象再将映射添加到其中。
设置实例
除了通过initialValue()方法设置初始值,还可通过 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);
}
通过getMap(t)
获取该线程的 ThreadLocalMap 对象后,直接将该 ThreadLocal 对象this
与对应实例value
的映射添加进该线程的 ThreadLocalMap中。后面的if-else与上文“设置初始值”中同理。
防止内存泄漏
针对上文提到的:“实例Object v
被该 Entry 引用而无法被回收造成内存泄漏”,是怎么解决的呢?
其实很简单。在ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。
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();
}