ThreadLocal

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

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

ThreadLocal中的方法

在JDK1.5以后,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。
ThreadLocal<T>类提供了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版本中还多了如下的这个方法:

    /**
     * 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内部是如何为每一个线程维护变量副本的呢?
在ThreadLocal类中有一个静态内部类ThreadLocalMap(概念上类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

源代码分析

    /**
     * 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的变量值
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();    //若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
    }

    // 是调用当期线程t,返回当前线程t中的一个成员变量threadLocals。
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // java.lang.Thread类下, 实际上就是一个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进行操作,下面看一下该类的源代码:

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的数组,将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 api作一个切面,存放一些如用户名等用户信息,在连接点方法结束后,再显式调用remove。

ThreadLocal的一个使用示例

测试类:ThreadLocalTest.java

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

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

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

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);
    }


}

运行结果

Thread-0新增订单服务中获取用户id-->1
Thread-1新增订单服务中获取用户id-->2
Thread-0在更新库存中获取用户id-->1
Thread-1在更新库存中获取用户id-->2
阅读 1.4k

推荐阅读
技术文档整理
用户专栏

后端开发

21 人关注
31 篇文章
专栏主页