吊打面试官之 ThreadLocal 详解
吊打面试官之 ThreadLocal 详解
ThreadLocal 的基本原理
我们先看一下 ThreadLocal 的简单使用:
ThreadLocal<String> localName = new ThreadLocal();
localName.set("帅枫");
String name = localName.get(); // name = 帅枫
localName.remove();
从代码上看它的使用很简单,它可以实现线程间的数据隔离,一个线程使用 get()方法是不能拿到其他线程的值的。我们可以看一下 set()方法的源码:
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取 ThreadLocalMap 对象,可以知道线程里面有一个 ThreadLocalMap 类型的字段
ThreadLocalMap map = getMap(t);
if (map != null)
// 不为空就这设置值
map.set(this, value);
else
// 为空就创建一个 map 对象
createMap(t, value);
}
set()源码很简单,我们看看里面的 ThreadLocalMap 是个什么东东:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们知道获取的是当前线程中的一个 threadLocals 变量,那这又是个啥呢?
public class Thread implements Runnable {
……
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
……
}
我们可以看到,每个线程里面都有一个 threadLocals 变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程 Thread 的 threadLocals 变量里面的,别人没办法拿到,从而实现了隔离。
ThreadLocalMap 看起来像是一个 Map,其实不然,我们直接看源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
......
}
可以看到,里面还有一个内部类 Entry,是继承了弱引用了的,我们上面 set() 设置的值其实就是存储到了 Entry 里面的 value 这个字段了,其实ThreadLocalMap 里面的 set()方法里是一个 Entry 数组。但是我们发现 Entry 中并没有链表一样的指针,那他是怎么解决 Hash 冲突的呢?
我们看下 set()源码就知道了:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 如果槽不为空,就一个向后找直到为空
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果不为空并且这个 Entry 对象的 key 正好是即将设置的 key,那么就直接更新 value
if (k == key) {
e.value = value;
return;
}
// 如果当前位置是空的,就初始化一个 Entry 对象放在该位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
解决 Hash 冲突除了拉链法还有一个是开放定址法,这里就是使用了开放定址法,如果该槽的位置存在元素了,就会顺着向后找,知道有一个是空的,这个 set()方法正式使用了这种方法。但是这个方法也是有缺陷的:
在使用 get()方法的时候,也会根据 ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的,相当于是线性级别的了。
下面看看 getEntry()源码:
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);
}
这里我们简单的提一下:我们都知道线程都有一个自己的栈,它是线程私有的,而堆是线程公有的,由 ThreadLocal 的作用可知,那存储在 ThreadLocal 中的对象是不是存储在栈中呢?
其实不是的,它们都还是位于堆上的,只是通过一些技巧将可见性修改为了线程可见。
ThreadLocal 的内存泄露问题
为啥会存在内存泄露问题呢?
我们上面讲解了 Entry 是继承是弱引用的,并且里面的泛型就是 ThreadLocal 类型,ThreadLocal 在保存的时候会把自己当做 key 存在ThreadLocalMap中,正常情况应该是 key 和 value 都应该被外界强引用才对,但是现在key 被设计成 WeakReference 弱引用了。
我们先简单的介绍一些弱引用:
被弱引用的对象,不管内存是否足够,只要执行垃圾回收,就会回收弱引用对象。
这样,如果发生垃圾回收,并且创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,从而发生内存泄露。那怎么解决呢?
在代码的最后使用 remove 就好了,就像下面这样:
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("帅枫");
……
} finally {
localName.remove();
}
remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
既然使用弱引用会导致内存泄露,那么为什么 ThreadLocalMap 的 key 要设计成弱引用?
key 不设置成弱引用的话就会造成和 entry 中 value 一样内存泄漏的场景。
ThreadLocal 的应用场景
首先就是 Spring 中的事务隔离级别使用了 ThreadLocal,Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
如果我们想在一次请求中保持用户的登录信息,我们也是可以使用 ThreadLocal 的。
引用: