最近面试会被经常问到ThreadLocal的作用和原理,所以这里有必要学习一下。
什么是ThreadLocal
ThreadLoal 被称为线程局部变量,即该变量运行在线程中时,每个线程都独立拥有它而不和其他线程中的这个值相冲突,其目的就使这个变量只属于当前线程,和其他线程无关。ThreadLoal 解决的是变量在不同线程间的隔离性,也就是说不同线程拥有自己的值。
ThreadLocal实现原理
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
一个线程内可以存在多个 ThreadLocal 对象,因为在 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 内部定义的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。
首先我们先看 get() 方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
首先取得当前线程,然后通过getMap(Thread t)方法获取到一个map,map的类型为ThreadLocalMap。
如果map不为空,接着获取ThreadLocalMap.Entry对象。ThreadLocalMap.Entry对象是以this指向的ThreadLocal对象为键在ThreadLocalMap中进行查找的。
如果获取到的ThreadLocalMap.Entry不为空,则返回value值,即和当前线程绑定的值。
如果map为空或者ThreadLocalMap.Entry为空,则调用setInitialValue()方法设置初始值。
接下来我们将上面的每一句来仔细分析:首先看一下getMap(Thread t)方法中做了什么:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
在getMap(Thread t)中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。Thread类中的成员变量threadLocals是什么?源码如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
threadLocals是一个ThreadLocalMap,它是ThreadLocal类的一个静态内部类,ThreadLocalMap的实现如下:
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;
}
}
...
}
可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键。
setInitialValue()的实现如下:
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;
}
protected T initialValue() {
return null;
}
先调用initialValue()方法获取初始值,默认返回null。然后获取和当前线程相关的ThreadLocalMap,如果map不为空,就设置键值对,如果map为空,调用createMap(Thread t, T firstValue),createMap(Thread t, T firstValue)的实现如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取,每个线程中都有一个独立的ThreadLocalMap,它所存储的值只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发问题。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
这个时候再看set(T 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);
}
在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,其中以this指向的threadlocal对象为健值,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。
总结一下:
- 通过ThreadLocal设置的对象是存储在每个线程自己的ThreadLocalMap类型的成员变量threadLocals中的;
- 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量;
- 我们发现如果没有先set的话,即在map中查找不到对应的ThreadLocalMap.Entry,则会通过调用setInitialValue()方法,而在setInitialValue()方法中,有一条语句是T value = initialValue(), 而默认情况下,initialValue()方法返回的是null。所以如果想在get之前不需要调用set就能返回值的话,必须重写initialValue()方法。
内存泄漏问题
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以 ThreadLocal 如果没有被外部强引用的情况下,会在垃圾回收的时候被清理掉,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
所以最佳实践,应该在我们不使用的时候,主动调用remove方法进行清理。
阿里开发规范建议如下:
这里把ThreadLocal定义为static还有一个好处就是,由于ThreadLocal有强引用在,那么在ThreadLocalMap里对应的Entry的键会永远存在,那么执行remove的时候就可以正确进行定位到并且删除!!!
ThreadLocal用在什么地方
讨论ThreadLocal用在什么地方前,我们先明确下,如果仅仅就一个线程,那么都不用谈ThreadLocal的,ThreadLocal是用在多线程的场景的。
ThreadLocal归纳下来就2类用途:
- 保存线程上下文信息,在任意需要的地方可以获取。
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失。
保存线程上下文信息,在任意需要的地方可以获取
由于ThreadLocal的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。
比如每个请求怎么把一串后续操作关联起来,就可以用ThreadLocal进行set,在后续的任意需要记录日志的方法里面进行get获取到请求id,从而把整个请求串起来。
还有比如Spring的事务管理,用ThreadLocal存储Connection,从而各个DAO可以获取同一Connection,可以进行事务回滚,提交等操作。
线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失
由于不需要共享信息,自然就不存在竞争问题了,从而保证了某些情况下线程的安全。ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。这类场景阿里规范里面也提到了:
但是ThreadLocal也有局限性,我们来看看阿里规范:
每个线程往ThreadLocal中读写数据是线程隔离,互相之间不会影响的,所以ThreadLocal无法解决共享对象的更新问题!
参考:
https://blog.csdn.net/dakaniu/article/details/80829079