Java进阶—ThreadLocal

是什么?

ThreadLocal 源码解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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).

大概的理解是:ThreadLocal类用来提供线程内部的本地变量。这些变量在每个线程内会有一个独立的初始化的副本,和普通的副本不同,每个线程只能访问自己的副本(通过get或set方法访问)。在一个类里边ThreadLocal成员变量通常由private static修饰。

简单地说,ThreadLocal的作用就是为每一个线程提供了一个独立的变量副本,每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

我们必须要区分ThreadLocal和Synchronized这种同步机制,两者面向的问题领域是不一样的。sysnchronized是一种互斥同步机制,是为了保证在多线程环境下对于共享资源的正确访问。而ThreadLocal从本质上讲,无非是提供了一个“线程级”的变量作用域,它是一种线程封闭(每个线程独享变量)技术,更直白点讲,ThreadLocal可以理解为将对象的作用范围限制在一个线程上下文中,使得变量的作用域为“线程级”。

没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。

几个重要方法

在JDK1.5以后,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。
ThreadLocal类提供了4个可用的方法(基于JDK1.7版本):

  1. void set(T value)设置当前线程的线程本地变量的值。
  2. public T get()该方法返回当前线程所对应的线程局部变量。
  3. public void remove()将当前线程局部变量的值删除。
    该方法是JDK 5.0新增的方法,目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  4. protected T initialValue()返回该线程局部变量的初始值。
    该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
    这个方法是一个延迟调用方法,在线程第1次调用get()或set(T value)时才执行,并且仅执行1次,ThreadLocal中的缺省实现是直接返回一个null。

可以通过上述的几个方法实现ThreadLocal中变量的访问,数据设置,初始化以及删除局部变量。

注意,在JDK1.8版本中还多了如下的这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Creates a thread local variable. The initial value of the variable is
* determined by invoking the {@code get} method on the {@code Supplier}.
*
* @param <S> the type of the thread local's value
* @param supplier the supplier to be used to determine the initial value
* @return a new thread local variable
* @throws NullPointerException if the specified supplier is null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}

原理

ThreadLocal原理示意图
从上图我们可以初步窥见ThreadLocal的核心机制:

  1. 每个Thread线程都持有一个ThreadLocalMap(概念上类似于Map),属性名是threadLocals
  2. ThreadLocalMap里的key是一个ThreadLocal对象,value是对应该线程的变量副本(即ThreadLocal的泛型类型的实例)。
    :每个线程是可能存在多个ThreadLocal的。
  3. ThreadLocalMap是由ThreadLocal维护的,通过ThreadLocal对象的get方法从ThreadLocalMap获取value,通过set方法设置ThreadLocalMap中的value。

这样对于不同的线程,每次都只能获取当前线程的变量副本值,从而可以做到副本隔离,互不干扰。

源代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
   /**
* 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); //获取当前线程对应的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); //获取当前ThreadLocal对象在ThreadLocalMap中对应的Entry
if (e != null)
return (T)e.value;
}
return setInitialValue(); //若当前线程还未在ThreadLocalMap设置过值,则在ThreadLocalMap中设置一个初始值并返回。如果当前线程还未持有ThreadLocalMap示例,则还需要调用createMap方法创建一个ThreadLocalMap对象,并赋值给线程对象的threadLocals属性。
}

//返回当前线程所持有的ThreadLocalMap对象。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

// java.lang.Thread类中有一个名为threadLocals, 类型为ThreadLocalMap的属性
ThreadLocal.ThreadLocalMap threadLocals = null;


/**
* 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;
}


/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}


/**
* 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
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}


/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* <tt>initialValue</tt> method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

上述是在ThreadLocal类中的几个主要的方法,他们的核心都是对其内部类ThreadLocalMap进行操作,下面看一下该类的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static class ThreadLocalMap {

//map中的每个节点Entry,它的key是ThreadLocal对象并且还是弱引用,这也导致了后续会产生内存泄漏问题。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
/**
* 初始化容量为16,以为对其扩充也必须是2的指数
*/
private static final int INITIAL_CAPACITY = 16;

/**
* 真正用于存储的数组,将ThreadLocal和其对应的值包装为一个Entry。
*/
private Entry[] table;


///....其他的方法和操作都和map的类似
}

ThreadLocal的几个问题

为什么不直接用线程id来作为ThreadLocalMap的key?

这个问题很容易解释,因为一个线程中可以有多个变量副本(也即需要多个ThreadLocal对象),所以ThreadLocalMap中会有多个键值对,来存储多个value值,而如果使用线程id作为key,那就只有一个键值对了。

ThreadLocal的内存泄露问题

首先要理解内存泄露(memory leak)和内存溢出(out of memory)的区别。内存溢出是因为在内存中创建了大量在引用的对象,导致后续再申请内存时没有足够的内存空间供其使用。内存泄露是指程序申请完内存后,无法释放已申请的内存空间,(不再使用的对象或者变量仍占内存空间)。

根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,造成内存泄露。
只有当前thread结束以后, Thread Ref就不会存在栈中,强引用断开, Thread, ThreadLocalMap, Entry将全部被GC回收。但如果是线程对象不被回收的情况,比如使用线程池,线程结束是不会销毁的,就可能出现真正意义上的内存泄露。

ThreadLocalMap设计时的对上面问题的对策:
当我们仔细读过ThreadLocalMap的源码,我们可以推断,如果在使用的ThreadLocal的过程中,显式地进行remove是个很好的编码习惯,这样是不会引起内存泄漏。
那么如果没有显式地进行remove呢?只能说如果对应线程之后调用ThreadLocal的get和set方法都有很高的概率会顺便清理掉无效对象,断开value强引用,从而大对象被收集器回收。

但无论如何,我们最好还是能够在适当的时机去显式调用ThreadLocal的remove方法。一个比较熟悉的场景就是对于一个请求一个线程的server如tomcat,在代码中增加一个拦截器或者web 切面类,在请求开始时存放一些如userId等信息,在请求结束时再显式调用remove。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class LocalInterceptor implements HandlerInterceptor {


@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ApplicationContext context = ThreadContext.getContext();
if (context == null) {
context = new ApplicationContext()
ThreadContext.setContext(context);
}
String token = request.getParameter("token");
Long userId = TokenUtils.parseToken(token);
context.setUserId(userId);
return true;
}


@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ApplicationContext context = ThreadContext.getContext();
if (context != null) {
ThreadContext.removeContext();
}
}
}

注意:tomcat中处理请求的线程是在线程池中的,某个线程处理完一个请求之后,会被重新放入线程池中,并不会被销毁掉,那这个线程对应的变量副本也仍然存在。所以每个请求我们都需要重新new一个变量副本,放入ThreadLocal对象中去,并且在请求完成后进行remove操作,释放内存空间。

使用示例

测试类:ThreadLocalTest.java

启动两个线程,第一个线程中存储的userid为1,第二个线程中存储的userid为2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.lzumetal.multithread.threadlocal;

import java.math.BigDecimal;

public class ThreadLocalTest {

public static void main(String[] args) throws InterruptedException {
Order order01 = new Order(1, 1, new BigDecimal(10), 1);
new Thread(new OrderHandler(1, order01)).start();

Order order02 = new Order(2, 2, new BigDecimal(20), 2);
new Thread(new OrderHandler(2, order02)).start();
}
}

OrderHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.lzumetal.multithread.threadlocal;

public class OrderHandler implements Runnable {

private static OrderService orderService = new OrderService();

private Integer userId;
private Order order;

public OrderHandler(Integer userId, Order order) {
this.userId = userId;
this.order = order;
}

@Override
public void run() {
EnvUtil.getUserIdContext().set(userId);
orderService.addOrder(order);
orderService.updateStock(order.getGoodId(), order.getGoodCount());
}
}

OrderService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.lzumetal.multithread.threadlocal;

public class OrderService {


/**
* 新增订单
*
* @param order
*/
public void addOrder(Order order) {
Integer userId = EnvUtil.getUserIdContext().get();
System.out.println(Thread.currentThread().getName() + "新增订单服务中获取用户id-->" + userId);
}


/**
* 更新库存
*
* @param goodId
* @param goodCount
*/
public void updateStock(Integer goodId, Integer goodCount) {
//虽然更新库存不需要关注userId,但是在这里也一样能够获取到
Integer userId = EnvUtil.getUserIdContext().get();
System.out.println(Thread.currentThread().getName() + "在更新库存服务中获取用户id-->" + userId);
}


}

运行结果

1
2
3
4
Thread-0新增订单服务中获取用户id-->1
Thread-1新增订单服务中获取用户id-->2
Thread-0在更新库存服务中获取用户id-->1
Thread-1在更新库存服务中获取用户id-->2
------ 本文完 ------