在Java中,对象是线程共享的,当我们想让线程独自拥有专属于自己的变量时,可以使用ThreadLocal类。
ThreadLocal类提供了线程局部(Thread-Locak)变量。这些变量不同于他们的普通对应物,因为访问某个变量的每个线程都有自己的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本。所以,每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
从线程的角度看,目标变量就是线程的本地变量,这也是Local所代表的含义。
一、使用方法
- public T get():返回此线程局部变量的当前线程副本中的值。如果变量没有用于当前线程的值,则先将其初始化为调用 initalValue() 方法返回的值。
- protected T initialValue():返回此线程局部变量的当前线程的”初始值“。线程第一次使用 get() 方法会调用此方法。但如果线程之前调用了 set() 方法,则不会对该线程再调用 initialValue() 方法。通常,此方法对每个线程最多调用一次,但如果在调用 get() 后又使用 remove() 方法,则可能再次调用此方法。
- public void remove():移除此线程局部变量当前线程的值。如果此线程局部变量随后被当前线程读取,且这期间线程没有设置其值,则将调用其 initialValue() 方法重新初始化其值。这将导致在当前线程多次调用 initialValue() 方法。
- public void set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。大部分子类不需要重写此方法,它们只依靠 initialValue() 方法来设置线程局部变量的值。
二、ThreadLocal实现原理
ThreadLocal.ThreadLocalMap threadLocals = null; //Thread对象中含有字段 ThreadLocalMap
get方法
get方法返回当前线程中,存有的ThreadLocal局部变量。如果当前线程中没有存入该值,将调用initialValue方法返回的值。源码如下:
public T get() {
Thread t = Thread.currentThread(); //当前线程
ThreadLocalMap map = getMap(t); //获取该线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //获取map中键为该ThreadLocal的entry
if (e != null) { //若有值 则返回该值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); //若没有该值 则调用initialValue方法
}
getMap(Thread t) 方法用来获取线程中的ThreadLocalMap:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; //获取线程t的局部变量表
}
setInitialValue()方法将获取初始值,并往线程的局部变量表中填入threadLocal-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;
}
set方法
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);
}
remove方法
remove方法将清除线程局部变量表中的该threadLocal键值对。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
以上几个方法的核心都是对线程的ThreadLocalMap进行操作,下面我们将查看ThreadLocalMap的源码。
三、ThreadLocalMap实现原理
static class ThreadLocalMap {
//map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
static class Entry extends WeakReference<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了一个对象。
四、内存泄漏问题
网上大家讨论说ThreadLocal会导致内存泄漏,原因如下:
- 首先ThreadLocal实例被线程的ThreadLocalMap实例持有,也可以看成被线程持有。
- 如果应用使用了线程池,那么之前的线程实例处理完之后出于复用的目的依然存活
- 所以,ThreadLocal设定的值被持有,导致内存泄露。