Java之ThreadLocal

什么是ThreadLocal

注意:JDK1.8版本

首先看下ThreadLocal在源码中的介绍:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* <p>For example, the class below generates unique identifiers local to each
* thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.

我们可以这样理解:ThreadLocal提供了线程的局部变量,每个线程都可以通过set()**和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离**。

简而言之:往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。

ThreadLocal 的主要是封装变量起到线程隔离的作用,另外 ThreadLocal 通常会用 private 和 static 来修饰。

##ThreadLocal的使用场景

根据 ThreadLocal 线程隔离的特性,主要有以下几种场景:

  • 保存一个多层调用的参数。想象一下这样的场景,有一个变量,需要通过很多层调用才能最终使用到,但是这中间的每一层方法调用都需要传递这个变量作为方法的入参,这样写虽然可以达到目的,但是却不够美观,如果可以用到 ThreadLocal,随用随取,就会优雅很多。
  • 保存上下文信息,如知名的 APM 系统 SkyWalking 就用到了 ThreadLocal 来保存 context,传递调用信息,还有 log4j 的 MDC 机制,也是基于 ThreadLocal 来实现的。
  • 由于 ThreadLocal 保存的变量是每个线程私有的,所以也可以一定程度上保证线程安全,比如 SimpleDateFormat 就是线程不安全的,所以生产环境上就经常使用 ThreadLocal 来封装。

使用简单案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserContext {
private static ThreadLocal threadLocal = new ThreadLocal();

public static User getUser() {
return (User) threadLocal.get();
}

public static void setUser(User user) {
threadLocal.set(user);
}


public static void remove() {
threadLocal.remove();
}
}

主要是三个方法,set,get 以及 remove。不要忘记 remove,不然容易造成内存泄漏。

ThreadLocal实现的原理

首先,看下ThreadLocal的set()方法,一般使用都是new完对象,就往里面set对象了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void set(T value) {

// 得到当前线程对象
Thread t = Thread.currentThread();

// 这里获取ThreadLocalMap
ThreadLocalMap map = getMap(t);

// 如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

上面有个ThreadLocalMap,看一下这个是什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//....很长
}

通过上面我们可以发现的是ThreadLocalMap是ThreadLocal的一个内部类。用Entry类来进行存储

值都是存储到这个Map上的,key是当前ThreadLocal对象

如果该Map不存在,则初始化一个:

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果该Map存在,则从Thread中获取

1
2
3
4
5
6
7
8
9
10
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

Thread维护了ThreadLocalMap变量

1
2
3
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null

从上面又可以看出,ThreadLocalMap是在ThreadLocal中使用内部类来编写的,但对象的引用是在Thread中

于是我们可以总结出:Thread为每个线程维护了ThreadLocalMap这么一个Map,而ThreadLocalMap的key是LocalThread对象本身,value则是要存储的对象

查看get()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}

ThreadLocal原理总结

  1. 每个Thread维护着一个ThreadLocalMap的引用
  2. ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
  3. 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值是传递进来的对象
  4. 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
  5. ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value

正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。

避免内存泄露

在使用 ThreadLocal 的时候,ThreadLocal 变量作为 key(实际上是一个弱引用),实际存储的值作为 value,存储到了 ThreadLocalMap 中,如下图,图中的虚线表示弱引用。

image-20201124174056250

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用

想要避免内存泄露就要手动remove()掉

注意这里的 ThreadLocalMap 中的 Entry 继承了 WeakReference,也就是弱引用。至于弱引用,可以参考《深入理解 Java 虚拟机》书中四种引用强、软、弱、虚的定义:

  • 强引用类似 Object obj = new Objecg() 这类,只要有就不会被回收。
  • 软引用用来描述一些还有用但并非必需的对象。在内存溢出之际,会把这些对象列进回收范围进行第二次回收。如果回收后还不够,才会抛出内存溢出异常。
  • 弱引用也是描述非必须对象的,但比软引用更弱。垃圾收集器工作时即使内存够用也会回收。
  • 虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。被虚引用关联的对象呗收集器回收时会收到一个系统通知。