前言
在我们学Java的时候,知道变量通常有成员变量和局部变量。局部变量在一个方法内部使用,当方法结束后,局部变量也就被清除;成员变量属于一个对象,只要我们有一个对象的访问权限,我们就可以访问这个对象的成员变量。
ThreadLocal又叫本地线程变量,是相对于以上两种变量的另一种模式。ThreadLocal和一个线程的生命周期绑定,一个线程从一个顶级方法入口进入,一层层的调用又一层一层的退出,最终从顶级方法入口结束。局部变量的作用域是线程域的某个方法内部;成员变量的作用域虽然很宽广,但是你必须拥有这个对象访问权限;本地线程的变量的作用域,属于整个线程范围,一个线程可以跨越多个方法使用本地线程变量,这就是它特殊的地方。
概念
如何要定义一个横跨多个方法,线程全局使用的对象。前面说了局部变量和成员变量都不行。因此我们要把本地线程变量放在一个公共区域,也就是说我们线程无论运行到哪个方法中,都可以访问这个共享线程变量。
public class ThreadLocalContext {
public static final ThreadLocal<Integer> threadLocalVariable = new ThreadLocal<>();
}
我们定义了一个类,它有一个static final方法,是一个整数类型的本地线程变量。所以无论你的线程走到哪里,都可以通过ThreadLocalContext.threadLocalVariable来进行访问这个整数。你可能会有疑惑,既然如此那我干嘛不用下面这种形式?
public class ThreadLocalContext {
public static final ThreadLocal<Integer> threadLocalVariable = new ThreadLocal<>();
public static final int a = 0;
}
我们定义一个static final int a
变量,它也是整数,我也可以在任何一个方法中访问和修改这个a,也实现了多个方法中共享使用。这两种模式虽然都可以在多个方法中共享,但是后者并不能做到线程的隔离。但是我线程1把a设置成了0,拟后面整个生命周期中都采用int a = 0的形式,但是这样存在线程安全问题。我希望的是这个a只是被某一个线程在多个方法中共享,而不是希望这个a在多个线程中共享。
ThreadLocal<Integer>可以看做是一种特殊的Integer,它比int a
多了一种特殊的功能。线程1拿到这个ThreadLocal<Integer>整数对象,给她set了一个值100;其他线程拿到ThreadLocal<Integer>也可以设置另一个值90,它们并不会覆盖。线程1在后续任何方法中拿到这个ThreadLocal<Integer>对象get出来都是100,而线程get出来依然是90。
也就是说ThreadLocal<Integer>它从语义上讲和int a没有任何区别,使用上是等价的,但是它多了一个特殊功能就是线程隔离。不同线程访问它,给他赋值可以做到相互不干扰。
实现
每一个Thread线程对象的内部,都维护了一个ThreadLocalMap
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
set
当我们要threadLocalVariable.set(100)
设置一个值的时候
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 获取当前线程,拿到Thread
- 拿到当前线程的ThreadLocalMap
- map.set(this, value)设置值
在第三步设置值中出现了一个this,这个this表示ThreadLocal对象本身它作为key存在,而value是我们set进来的value保存在ThreadLocalMap的value上。
get
如果调用一个ThreadLocal对象的get方法,只需要把this传进去作为key,搜索ThreadLocalMap上的value。每一个操作都是先获取当先线程Thread对象,然后根据ThreadLocal对象作为key,去Thread对象内部的Map寻找这个value。这就是线程隔离的关键。
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();
}
无论你多少个线程,即便同时去操作ThreadLocal对象的get或set方法,他们根本不会调用ThreadLocal对象的任何方法,只会把ThreadLocal对象当做一个key罢了。
为什么要用弱引用作为key
我们往ThreadLocal对象上set了一个值,本质上是往ThreadLocalMap中添加了一个entry,如果map的key是一个强引用,即便你在程序的任何地方都拿不到这个key的引用了,但是ThreadLocalMap中key指向了这个ThreadLocal对象,那么永远都不会被垃圾回收。因为有一个强引用的key指向了它。
// 弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 强引用
static class Entry {
ThreadLocal<?> key;
Object value;
Entry(ThreadLocal<?> k, Object v) {
key = k;
value = v;
}
}
如果程序中一旦再也不存在一个强引用指向ThreadLocal对象,说明程序中已经不可访问这个ThreadLocal了。那么这个key所指向的堆上的ThreadLocal对象就被垃圾回收了,key就指向了一个null,如果还不清楚,就看下面这个例子:
public class Test {
private static Map<Object, Object> map = new HashMap<>();
public static void put() {
map.put(new Object(), new Object());
}
}
map是一个全局存在,通过put设置了一个Node,如果不通过迭代器我们怎么也访问不到他们,因为key在我们调用put后就已经拿不到。所以ThreadLocalMap中使用弱引用作为key,是为了当程序中一旦不存在一个强引用指向ThreadLocal对象,说明程序中已经不能访问这个ThreadLocal了,那么这个key所指向的堆上的ThreadLocal对象就被垃圾回收了,key就指向了一个null。那么对上叙代码进行改造的话:
public class Test {
private static Map<Object, Object> map = new HashMap<>();
public static void put() {
map.put(new WeakReference<>(new Object()), new Object());
}
}
一旦key的这个Object没有强引用指向了,那么map中的弱引用key就会指向一个null。