1. ThreadLocal
1.1 ThreadLocal 是什么?
之前介绍的线程同步的三种方式,无论是轻量级的 Atomic、volatile,还是 synchronized,其实都是采用同步的方式解决了线程安全问题。而 ThreadLocal 是另外一种解决线程安全的思路,即线程封闭。
多线程并发时,对于一个共享变量的访问是不安全的,而方法内的局部变量是线程安全的,每个线程都会有自己的副本。ThreadLocal 就是介于这两者之间,既能保证线程安全,又不至于局限于方法内部的方式。ThreadLocal 变量的作用域是为线程,也就是说它可以做到线程内跨方法共享。
1.2 ThreadLocal 的实现原理
- 每一个 ThreadLocal 都有一个唯一的 ThreadLocalHashCode;
- 每一个线程中有一个专门保存这个 HashCode 的 ThreadLocalMap:
ThreadLocal.ThreadLocalMap threadLocals = null;
。它存放了与本线程相关的所有自定义信息,对这个变量的定义在 Thread 类,而操作却在 ThreadLocal 类中。 当
ThreadLocal#get()
时,实际上是根据当前 ThreadLocal 对象的 ThreadLocalHashCode 作为键,从保存在当前线程中的 threadLocals (threadLocals 是 ThreadLocalMap)取值。- 即每个线程对应的变量不是存储在 ThreadLocal 对象中的,而是存在当前线程对象中的,每个线程自己保管封存在自己内部的变量,达到线程封闭的目的。
也就是说,ThreadLocal 对象并不负责保存数据,它只是一个访问入口。
2. ThreadLocal 简单示例
用一段小代码来试验一下:
public class threadlocal {
// 这里创建的threadLocal会自动在每一个线程上创建一个副本,副本之间彼此独立,互不影响
// 这里创建的threadLocal就是ThreadLocalMap中的key
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public void threadLocalTest() throws InterruptedException {
threadLocal.set("这是主线程设置的123");
String v = threadLocal.get();
System.out.println("线程1执行之前,主线程取到的值: " + v);
new Thread(() -> {
String v1 = threadLocal.get();
System.out.println("线程1取到的值:" + v1);
threadLocal.set("这是线程1设置的456");
v1 = threadLocal.get();
System.out.println("重新设置之后线程1取到的值: " + v1);
System.out.println("线程1执行结束");
}).start();
// 等待所有线程执行结束
Thread.sleep(5000);
System.out.println("--------------------------");
v = threadLocal.get();
System.out.println("线程1执行之后主线程取到的值: " + v);
}
public static void main(String[] args) throws InterruptedException {
new threadlocal().threadLocalTest();
}
}
```java 执行结果为: 线程1执行之前,主线程取到的值: 这是主线程设置的123 线程1取到的值:null 重新设置之后线程1取到的值: 这是线程1设置的456 线程1执行结束
线程1执行之后主线程取到的值: 这是主线程设置的123
可以看到,主线程和线程1中的值是无关的。
<a name="2d87dc8c"></a>
# 3. set() 和 get() 的源码分析
![](https://cdn.nlark.com/yuque/0/2021/png/1032788/1614826965051-bc90e602-1e68-4652-a334-ca845cb11c9e.png#align=left&display=inline&height=515&id=gyChK&margin=%5Bobject%20Object%5D&originHeight=515&originWidth=880&size=0&status=done&style=none&width=880)
<a name="f1e0a0cd"></a>
## 3.1 set() 方法
```java
# Threadlocal类 Threadlocal.class
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// Thread类中有个成员变量ThreadLocalMap,拿到这个Map
ThreadLocalMap map = getMap(t);
if (map != null)
// this指的是ThreadLocal对象
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
// 获取线程的ThreadLocalMap
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
# Thread类 Thread.class
public class Thread implements Runnable {
//每个线程都有自己的ThreadLocalMap 成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;
}
在 set() 方法中,首先获取当前线程,然后取出当前线程的 ThreadLocalMap,然后以当前的 ThreadLocal 对象为 key(这里的 key 就是 this
),把值 value 存入当前线程的 ThreadLocalMap 对象中,每个线程的 ThreadLocalMap 对象是不一样的。这个部分还是比较绕的,上图比较直观的解释了这个过程。
总结一下,可以这么理解:每个线程都有自己的 ThreadLocalMap,每个线程的 ThreadLocalMap 以一个公用的 ThreadLocal 对象作为 key,将各自线程中的任意对象作为 value,存入线程各自的 ThreadLocalMap 中。
3.2 get() 方法
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();
}
在 get() 方法中,同样首先获取当前线程,然后取出当前线程的 ThreadLocalMap 对象,然后以当前的 ThreadLocal 对象为 key,从当前线程的 ThreadLocalMap 中取值,每个线程的 ThreadLocalMap 对象是不一样的。
4. 使用 ThreadLocal 时需要注意的问题
- ThreadLocal 变量一般要声名成 static 类型,即当前线程中只有一个T类型变量的实例,线程内可共享该实例数据且不会出问题,如将其声名成非 static,则一个线程内就存储多个T类型变量的实例,有点存储空间的浪费,一般很少有这样的应用场景。另外根据实际情况,ThreadLocal 变量声明时也常加上 private final 关键词表明它时类内私有、引用不可修改。
- 线程池环境下使用后将 threadLocal 变量 remove 掉或设置成一个初始值。在线程池环境下,由于线程是一直运行且复用的,使用 ThreadLocal 时会出现这个任务看到上个任务的 ThreadLocal 变量值以及内存泄露等问题,解决方法就是在当前任务执行完后将 ThreadLocal 变量 remove 或设置为初始值。
5. Netty 的 FastThreadLocal 是什么?
既然 Java 中有了 ThreadLocal 类了,为什么 Netty 还自己创建了一个叫做 FastThreadLocal 的结构?
问题就出在 ThreadLocalMap 类上,它虽然叫 Map,但却没有实现 Map 的接口。ThreadLocalMap 在 rehash的时候,并没有采用类似 HashMap 的数组+链表+红黑树的做法,它只使用了一个数组,使用开放寻址(遇到冲突,依次查找,直到空闲位置)的方法,这种方式是非常低效的。
由于 Netty 对 ThreadLocal 的使用非常频繁,Netty 对它进行了专项的优化。它之所以快,是因为在底层数据结构上做了文章,使用常量下标对元素进行定位,而不是使用 JDK 默认的探测性算法。
6. ThreadLocalMap 类为什么要定义在 ThreadLocal 类中,而不直接定义在 Thread 类中?
将 ThreadLocalMap 定义在 Thread 类内部看起来更符合逻辑,但是 ThreadLocalMap 并不需要 Thread 对象来操作,所以定义在 Thread 类内只会增加一些不必要的开销。定义在 ThreadLocal 类中的原因是 ThreadLocal 类负责 ThreadLocalMap 的创建,仅当线程中设置第一个 ThreadLocal 时,才为当前线程创建 ThreadLocalMap,之后所有其他 ThreadLocal 变量将使用一个 ThreadLocalMap。
总的来说就是,ThreadLocalMap 不是必需品,定义在 Thread 中增加了成本,定义在 ThreadLocal 中按需创建。
7. 为什么 ThreadLocalMap 的 key 被设计成弱引用?
为了尽最大努力避免内存泄露。如果使用线程池,那么线程的生命周期往往很长,根据 JVM 根搜索算法,一直存在 Thread -> ThreadLocalMap -> Entry 这样一条引用链路,如下图所示。
如果 Key 不设计成 WeakReference 类型,是强引用的话,就一直不会被 GC 回收,key 就一直不会是 null,不为 null Entry 元素就不会被清理(ThreadLocalMap 根据 Key 是否为 null 来判断是否清理 Entry)。所以 ThreadLocal 的设计者认为只要 ThreadLocal 所在的作用域结束了工作被清理了,GC 回收的时候就会把 Key 引用对象回收,Key 置为 null,ThreadLocal 会尽力保证 Entry 清理掉来最大可能避免内存泄露。
//元素类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; //key是从父类继承的,所以这里只有value
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//WeakReference 继承了Reference,key是继承了泛型的referent
public abstract class Reference<T> {
//这个就是被继承的key
private T referent;
Reference(T referent) {
this(referent, null);
}
}
Entry 继承了 WeakReference 类,Entry 中的 Key 是 WeakReference 类型的。ThreadLocal 对象被 WeakReference 类型的 Entry 中的 Key 引用,当 ThreadLocal 对象没有被其他对象强引用时,那么它会在发生 GC 时被回收掉。
这里有个问题,如果 ThreadLocal 一直有强引用应该怎么办,是不是有内存泄露的风险?最佳实践是用完 ThreadLocal 对象时手动调用 remove 函数。
class Threadlocal {
public void remove() {
//这个是拿到线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this); //this就是ThreadLocal对象,移除ThreadLocalMap中的ThreadLocal对象
}
}