0 快速理解
ThreadLocal的实现机制不复杂,它将自身实例作为key,和需要保存的value版本值,一起存入到当前线程所持有的一个map当中,代码可以简单的描写为(当然实际的代码并不是这样):
`Thread.currentThread().threadLocals.put(this, value);// threadLocal.set(T)
Thread.currentThread().threadLocals.get(this);// threadLocal.get()`
而每个线程都有自己的一个独立的map,这样就实现了每个线程独立地改变自己的副本,不会和其它线程冲突。
1 简介
ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。
线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
2 ThreadLocal的应用
- 在Spring中应用
Spring使用ThreadLocal解决线程安全问题。一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,这样有状态的Bean就可以在多线程中共享了。
- 在Slf4j 日志输出中的应用
Java Web项目中,通常使用实现了 Slf4j 的Logback或Log4j来进行日志输出,Slf4j 中定义了 MDC 接口,要求实现多线程间日志隔离,Logback 和 Log4j 正是利用ThreadLocal来实现的。更多内容将会在另一篇专门介绍MDC的文章中讲解。
3 实现原理
前面提到:
ThreadLocal将自身实例作为key,和需要保存的value(具体大变量副本值)一起存入到当前线程的一个map当中。
这段描述涉及三个类:Thread、ThreadLocal、ThreadLocalMap,它们实际的关系如下:
- ThreadLocal中声明了一个静态内部类ThreadLocalMap。可以将它视为一个map容器,key是ThreadLocal类型,value是Object。
- ThreadLocal类中定义了:
//静态内部类
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
...
}
- Thread类中拥有一个ThreadLocalMap类型的成员变量。这个成员变量ThreadLocalMap便是此线程用来存储各个ThreadLocal变量在本线程中具体副本值的。
- Thread类:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
//成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
下面来看一下ThreadLocal工作原理。
1) 给某个ThreadLocal变量, 设置一个与当前线程所绑定的具体值。
-> 使用ThreadLocal的set()方法:
/**
* Sets the current thread's copy of this thread-local variable to the specified value.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
其中:
(1)Thread.currentThread()是一个native方法,可以拿到当前的线程t。
(2)通过getMap(Thread t)方法获取当前线程所持有的ThreadLocalMap;
(3)然后将变量的副本值设置到这个ThreadLocalMap对象中(key=this,即当前ThreadLocal实例,value=传入的副本值value)。
(4)当然如果(2)中获取到的ThreadLocalMap对象为空,就通过createMap方法给线程t创建一个新ThreadLocalMap,并将key-value存入。
/**
* Get t
* he map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @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);
}
2) 获取某个ThreadLocal变量, 与当前线程所绑定的具体副本值。
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current 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);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
其中:
(1)Thread.currentThread()是一个native方法,可以拿到当前的线程t。
(2)通过getMap(Thread t)方法获取当前线程所持有的ThreadLocalMap;
(3)如果ThreadLocalMap不为空,则读取map中的value;
(4)如果ThreadLocalMap为空,则创建一个map,并返回初始化的值。
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial 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,存放着它自己所拥有的各个ThreadLocal变量副本值。
在外部代码中对ThreadLocal进行get或set时,能够获取到当前线程(它所在的线程),进而可以获取到当前线程对应的map,所以实际操作的一直是本线程所对应的ThreadLocalMap版本。每个线程的ThreadLocalMap只能自己被读取和修改。
这样在不同线程中对ThreadLocal进行操作时,实际修改和读取的是不同的ThreadLocalMap。从而实现了变量访问在不同线程中的隔离。
4 内存泄漏问题
为什么可能会有内存泄漏
Thread是通过ThreadlocalMap的key的形式对ThreadLocal产生了弱引用。(Thread的stack中的gcRoot开始可达性分析,能够发生对ThreadLocal的弱引用)。
当threadlocal实例的强引用都被释放以后, threadlocal实例本身可能会在gc时被清除。由于key不存在了,所以map里面对应的value永远不会被访问到了。然而value却没有被回收,所以存在着内存泄露。
WeakReference是Java语言规范中为了区别直接的对象引用而定义的另外一种引用关系。它的标志性的特点是:reference实例不会影响到被引用对象的GC回收行为(可以理解为弱引用,不在GC前对对象被引用情况的统计范围内),只不过在被对象回收之后,reference实例想获得被应用的对象时程序会返回null。
为什么ThreadLocalMap对key是弱引用
当threadLocal实例可以被GC回收。让key(threadLocal对象)为弱引用,当threadLocal实例被垃圾回收之后,key就变为null。再之后,系统可以通过“Entry不为null,而key为null”来判断该Entry对象应该被清理掉了。
如果对threadLocal不是弱引用,即使除了作为threadLocalMap的value对象即使在其他位置不再使用,也会被所引用导致无法被GC回收。
(来源:https://zhuanlan.zhihu.com/p/304240519)
扩展一下,Java中HashMap类型中对键是强引用。同时java也提供WeakHashMap,它对键是是弱引用。同样的在WeakHashMap中,当key可以被GC时,整个Entry都会被清除。
Entry的key被设计为弱引用就是为了让程序自动的对访问不到的数据进行回收提醒,所以,在访问不到的数据被回收之前,内存泄漏确实是存在的,但是我们不用担心,就算我们不调用remove,ThreadLocalMap在内部的set,get和扩容时都会清理掉泄漏的Entry,内存泄漏完全没必要过于担心。(但是放任不管不是最佳实践, 最好还是参考下面的做法)
避免内存泄漏的最佳实践
1)ThreadLocalMap会在set,get以及remove等方法中会对key已经被回收的entry做自动删除。
(set以及get不保证所有过期slots会在操作中会被删除,而resize则会删除threadLocalMap中所有的过期slots)
例如:在ThreadLocal的get方法中,使用了map.getEntry()方法,getEntry()中会对查找不到的key进行清除。
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);//此处着手清除key为null的Entry
}
2)最安全的办法是调用ThreadLocal的remove方法,把当前ThreadLocal从当前线程的ThreadLocalMap中移除(包括key,value)。来彻底避免可能的内存泄露。
每次使用完ThreadLocal,都调用它的remove()方法,清除无用数据。
5 总结
ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。
ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但是大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
ThreadLocal所保存的变量只能是Object类型,不能使用基本数据类型。
ThreadLocal的建议使用方法:
1)不要设计为static的。被class对象给强引用,线程存活期间就不会被回收,也不用remove,完全不用担心内存泄漏
2)设计为非static的,如果在长生命周期对象(比如被spring管理的对象)的内部,也不会被回收
3)没必要在方法中创建ThreadLocal对象;