ThreadLocal概述
学习一个东西首先要知道为什么要引入它,就是我们能用它来干什么。所以我们先来看看ThreadLocal对我们到底有什么用,然后再来看看它的实现原理。
ThreadLocal如果单纯从名字上来看像是“本地线程"这么个意思,只能说这个名字起的确实不太好,很容易让人产生误解,ThreadLocalVariable(线程本地变量)应该是个更好的名字。我们先看一下官方对ThreadLocal的描述:
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).
API表达了下面几种观点:
1、ThreadLocal不是线程,是用来提供线程内部的局部变量,你可以先简单理解为线程类的属性变量。
2、多线程环境中每个线程有自己的ThreadLocal的变量‘拷贝’,(通过get或set方法访问)时能保证各个线程里的变量线程相对独立于其他线程内的变量,修改本线程局部变量不影响其他线程中的局部变量。
3、ThreadLocal实例通常来说都是private static类型。
总结:
1、每个线程都有自己的局部变量
每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的(有前提,后面解释)
2、独立于变量的初始化副本
ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。
3、状态与某一个线程相关联
ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。
ThreadLocal适用于资源共享但不需要维护状态的情况,也就是一个线程对资源的修改,不影响另一个线程的运行;这种设计是“空间换时间”,synchronized顺序执行是“时间换取空间”。
ThreadLocal的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。
ThreadLocal实现原理
ThreadLocal可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
方法 | 描述 |
---|---|
T | get() 返回此线程局部变量的当前线程副本中的值。 |
protected T | initialValue() 返回此线程局部变量的当前线程的“初始值”。 |
void | remove() 移除此线程局部变量当前线程的值。 |
void | set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值。 |
可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量,那ThreadLocal内部是如何为每一个线程维护变量副本的呢?
其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。
ThreadLocal类源代码:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for thecurrent thread, it is first initialized to the
* value returned by an invocation of the {@link #initialValue} method.
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();//当前线程
ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//获取对应ThreadLocal的变量值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
}
//设置变量的值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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;
}
/**
* 为当前线程创建一个ThreadLocalMap的threadlocals,并将第一个值存入到当前map中
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//删除当前线程中ThreadLocalMap对应的ThreadLocal
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
上述是在ThreadLocal类中的几个主要的方法,他们的核心都是对其内部类ThreadLocalMap进行操作,下面看一下该类的源代码:
static class ThreadLocalMap {
//map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始化容量为16,以为对其扩充也必须是2的指数
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
*/
private Entry[] table;
///....其他的方法和操作都和map的类似
}
总之,为不同线程创建不同的ThreadLocalMap,用线程本身为区分点,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独new了一个对象。
内存泄漏问题
在上面提到过,每个thread中都存在一个map,map的类型是ThreadLocal.ThreadLocalMap。Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread,Map,value将全部被GC回收。
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
虽然ThreadLocalMap已经使用了weakReference,但是还是建议能够显示的使用remove方法。