ThreadLocal内部实现与使用
什么是ThreadLocal?
在JDK1.8的官方文档中有这样的解释:
/**
* 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).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
*/
总的来说,ThreadLocal是一个用于创建线程局部变量的类。
一般情况下,我们创建的变量是对所有线程可见的,任何线程都能够访问并修改这个变量,那这在并发环境下就会出现线程安全的问题。而ThreadLocal变量能够使访问这个变量的每个线程都获得一个变量的本地副本,实现变量的线程私有化。
关键方法
首先我们创建一个ThreadLocal对象 ThreadLocal<String> threadLocal = new ThreadLocal<>();
1.set()
然后在当前线程下使用set()方法设置一个String类型的值 threadLocal.set("test");
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);
}
首先获得当前线程 t ,然后通过getMap()方法得到线程 t 的ThreadLocalMap对象,ThreadLocalMap又是什么东西呢?它是ThreadLocal的一个静态内部类,可以看作是为ThreadLocal定制的HashMap,其中Entry的定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
需要注意的是其中作为 key 的 ThreadLocal 对象是一个弱引用。
接着再回到上面的 set() 方法源码,在 Thread 类中有一个初始值为 null 的 ThreadLocalMap 变量 threadlocals ,getMap() 方法返回的就是这个变量。如果 threadlocals 当前非空,说明 Map 已经被创建过,可以直接通过 ThreadLocalMap 的 set() 方法为当前线程set一个键值对,其中 key 为 ThreadLocal 对象,而Value在这里就是我们定义的 String 类型的变量 “ test ” 。如果为空,则为当前线程 t 的 threadlocals 变量新建 ThreadLocalMap 实例,同样地, key 为 ThreadLocal 对象,而Value在这里就是我们定义的 String 类型的变量 “ test ” 。
2.get()
先看一下源码中是如何定义 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() 方法首先会通过 getMap() 方法获得当前线程的 threadlocals 变量,如果非空,则访问它的 Entry ,从而获得对应的 value 值。否则,返回一个初始化值,setInitialValue() 方法会为当前的 threadlocals 变量设置一个 key 为 ThreadLocal 对象,value 为 null 的键值对,并返回 null 值。
3.remove()
先上源码: public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
第一步同样也是要通过 getMap() 获取当前线程的 threadlocals 变量,如果非空,则调用ThreadLocalMap中的 remove() 方法移除当前的 ThreadLocal 对象。这里的 this 指针就是当前调用这个 remove() 方法的那个 ThreadLocal 对象。
使用实例
下面展示一个简单的Demopublic class Demo {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
for(int i = 0; i < 5; i++){
String temp = String.valueOf(i);
new Thread(() -> {
String str = threadLocal.get();
threadLocal.set(str + "+" + temp);
System.out.printf("Thread name:%s , Value:%s, ThreadLocalHashCode:%s\n",
Thread.currentThread().getName(),
threadLocal.get(),
threadLocal.hashCode());
}).start();
}
}
}
控制台的输出结果:
Thread name:Thread-0 , Value:null+0, ThreadLocalHashCode:1516133987
Thread name:Thread-4 , Value:null+4, ThreadLocalHashCode:1516133987
Thread name:Thread-3 , Value:null+3, ThreadLocalHashCode:1516133987
Thread name:Thread-2 , Value:null+2, ThreadLocalHashCode:1516133987
Thread name:Thread-1 , Value:null+1, ThreadLocalHashCode:1516133987
从结果可以看出,不同的线程访问的是同一个 ThreadLocal 对象,它们通过 get() 方法得到的都是线程自己的 threadlocals 变量中的 value 值,如若不然,结果的 value 部分左侧不会一直是 null 值,而应该是上一个线程执行之后的结果。
线程与ThreadLocal对象的映射关系
内存泄漏
前面提到,在 ThreadLocalMap 的实现中,key 部分的 ThreadLocal 对象是一个弱引用,使用弱引用的原因是当没有强引用指向 ThreadLocal 变量时,它可以被回收,避免 ThreadLocal 对象不能被回收而造成内存泄漏。然而,这又出现了一个新的内存泄漏问题,弱引用一旦被回收,而 ThreadLocalMap 中 Value 部分又是强引用,这会出现 key 为 null 的情况,导致整个 Entry 无法被回收。
针对这个问题,ThreadLocal类的源码中给出了解决方案:
- 在 ThreadLocalMap 的 set() 方法中调用了 replaceStaleEntry() 方法将所有 key 为 null 的 Entry 的值设置为 null,从而使得该值可被回收。
- 在 ThreadLocalMap 的 getEntry() 方法中调用了 getEntryAfterMiss() 方法,同样会把 key 为 null 的 Entry 的值设置为 null。
- 如果我们在使用之后不再去调用 set() 和 get() 方法,也就意味着还有可能会发生内存泄漏,因此在用完 ThreadLocal 之后最好手动调用 remove() 方法来回收内存,避免出现内存泄漏。