一、常见的两种方法
set(T value)
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
可以看出在set方法中,首先是与获取当前线程相关的map数据结构(Thread类中肯定有这个成员变量的声明),类型却是ThreadLocal.ThreadLocalMap,其实查看源码可以知道,ThreadLocal.ThreadLocalMap的结构同HashMap结构类似。如下:
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int threshold; // Default to 0
}
}
可以看到,ThreadLocal.set(T value)会将value对象绑定到当前线程内部的ThreadLocal.ThreadLocalMap对象上,键名是ThreadLocal对象,键值是value对象。
1 这么来说,各个线程在进行set操作时,将目标变量绑定到线程自身的ThreadLocal.ThreadLocalMap对象上,线程之间是相互隔离,互不影响的,这一点其实跟线程安全并没有关系,线程安全是发生在多个线程同时访问某一共享变量时,出现线程之间相互影响导致出现错误结果的现象,而使用ThreadLocal访问的是线程自身内部的变量,所以说不会出现线程安全问题。
2 从ThreadLocal的set方法可以看出,它是将当前声明的共用变量作为key,也就是说在线程内部,只能维护一个与此ThreadLocal相关的Object对象,如果你想在当前线程绑定多个对象,那你就得声明多个ThreadLocal变量了。不过灵活的用法是绑定的数据结构是HashMap,而不是简简单单的Object。
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final ThreadLocal<HashMap<String, Object>> THREAD_LOCAL = new ThreadLocal<>();
2 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方法首先同set方法一样,获取当前线程的内部变量threadLocals(ThreadLocal.ThreadLocalMap类型),上面说过,它是一种类似HashMap的数据结构,可以根据key快速取出map结构中对应的value,不过这里的key就是ThreadLocal本身。如果threadLocals中没有此key,则返回初始化设置的默认值。
二、弱引用
我们知道Java中对象的引用分为四种,强引用,软引用,弱引用,虚引用,它们的引用强度依次降低。
JVM在回收垃圾对象时,是不对强引用对象进行回收的。那么软引用和弱饮用的区别在哪里?如果可用内存还没有降低到一个阈值临界点,JVM在扫描垃圾对象时,只会回收持有弱引用的对象,而不会回收持有软引用的对象。只有可用内存降低到阈值临界点时,JVM才会回收持有软引用的对象,所以说JVM在回收垃圾时,不管可用内存有没有降低到阈值临界点,都会回收持有弱引用的对象。
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int threshold; // Default to 0
}
}
从程序中可以看出ThreadLocalMap的Entry的key是持有ThreadLocal对象的弱引用的,假设某一时刻,JVM进行垃圾回收,那么这些Entry中的Key对象全部被清除,Entry中只剩下Value对象,但是却无法通过有效的Key来取出这些Value对象。由于Entry对象是强引用,所以这些Value对象会一直堆积在内存中,虽然ThreadLocal的set和get方法做了一些额外的处理,但是还是有内存泄露的风险。
另外,将ThreadLocal声明为static,是有一定道理的,我们知道类中的静态变量是有可能成为GC Root的,这样ThreadLocal是强引用,ThreadLocalMap中的Entry肯定也是强应用了,因为可以通过GC Root找到,所以Entry中的Key不会被JVM回收,那么ThreadLocal在执行remove的时候,可以正确定位到Entry并删除。正确的用法如下:
public void method1(String arg) {
LOGGER.info("invoke public method1.");
try {
THREAD_LOCAL.set(arg);
method2();
} finally {
THREAD_LOCAL.remove();
}
}
这一点,阿里巴巴集团开发手册规约(第14页第15条)上也明确提出:
- 【参考】ThreadLocal 无法解决共享对象的更新问题,ThreadLocal 对象建议使用 static
修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享
此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只
要是这个线程内定义的)都可以操控这个变量。
参考:
1 手撕面试题ThreadLocal,http://www.jiangxinlingdu.com/interview/2019/06/19/threadlocal.html。