ThreadLocal 类浅析(一)

阿凡达2018-06-28 09:06

网上有不少对ThreadLocal类的误读,而本人最近在学习JDK的部分源码,对ThreadLocal类也有一点浅显的理解。现整理如下,希望对大家能有一点帮助,错误之处,还请大家不吝赐教。

让我们先来看下ThreadLocal类的基本用法。

package study;
/**
 * Created by liwenshuai on 2017/9/26.
 */
public class TestThreadLocal {
    public static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        // 当没有进行set操作时,第一次get操作会调用 initialValue 函数。
        @Override        public Integer initialValue() {
            return Integer.valueOf(1);
        }
    };
    public static void main(String[] args) {
        // 启动两个注册到threadlocal的线程
        new Thread(new TestThread(1)).start();
        new Thread(new TestThread(2)).start();
    }
    static class TestThread implements Runnable {
        // 线程的id,用于区分各线程
        private int threadId;
        public TestThread(int threadId) {
            this.threadId = threadId;
        }
        @Override
        public void run() {
            for (int k = 100; k <=300; k += 100) {
                // 为了使累加前和累加后的值能成对输出,这里使用整个类来进行同步
                synchronized (TestThread.class) {
                    System.out.printf("累加前:当前线程 %d 的累加和为 %d,当前累加值为 %d\n", threadId, threadLocal.get(), k);
                    threadLocal.set(threadLocal.get() + k);
                    System.out.printf("累加后:当前线程 %d 的累加和为 %d\n", threadId, threadLocal.get());
                }
                try {
                    // 睡眠一段时间,使两个线程能够交替执行,便于观察结果。
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
    
    

程序的运行结果:

从结果中可以看到:两个线程拥有相同的初始值,但各自的getset操作是互不影响的。
在网上流传一种思路,ThreadLocal<T> 中包含了Map<Thread,T> 对象,其中保存了特定于线程的值,
ThreadLocal的实现并非如此,在JDK1.8中,可以说实现正好相反。下面让我们来具体看下ThreadLocal的实现。
1. ThreadLocal概况:
ThreadLocal的实现涉及到以下几个类:
Thread  JDK1.8中线程本地变量实际上保存在 Thread 类中。
ThreadLocalMap.Entry   保存键值对的对象,本质上是一个 WeakReference<ThreadLocal>对象,可以看作保存<键,值>的对象
ThreadLocalMap  通过哈希表方式实现高效的存储结构,类似于HashMap,本质上是一个数组,存储ThreadLocalMap.Entry实例。
SuppliedThreadLocal            本文未对其研究。

这些类的交互关系是:Thread类中有 ThreadLocalMap 的成员变量threadLocals,且初始值为null
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal类在进行set或者get操作时,通过线程Thread对象的实例获取到ThreadLocalMap,进而对该map进行增删该查操作。
ThreadLocalMap.Entry 对象的弱引用指向ThreadLocal,其value 成员变量保存真正要存储的值。
2. ThreadLocal的数据存取
我们知道,在使用ThreadLocal时,首先创建ThreadLocal对象,然后再调用其set(T)T get()方法。我们从这些点切入,首先是构造函数如下: /**  * Creates a thread local variable.  * @see #withInitial(java.util.function.Supplier)  */
public ThreadLocal() {
}
可以看到,构造函数没有任何实现。接下来我们再从set函数切入:
/**
 * 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);
//通过当前线程得到一个ThreadLocalMap
    if (map != null)
//map存在,则把value放入该ThreadLocalMap
        map.set(this, value);
    else
//map 不存在,则创建一个ThreadLocalMap
        createMap(t, value);
}
然后,看看getMap方法做了什么
/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    //返回Thread的一个成员变量
    return t.threadLocals;
}
正如前面所提到的,ThreadLocalMapThread绑定在了一起,Thread类中有一个ThreadLocalMapnull的变量,那我们现在回到ThreadLocalMap来看,在我们Thread返回的引用来看,如果mapnull的情况下,调用了createMap方法.这就为我们的Thread创建了一个能保存在本地线程的map.下面是createMap的源码
/**
 * 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
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
creatMap中会直接new 一个ThreadLocalMap,里面传入的是当前ThreadLocal,即this.然后创建一个大小为INITIAL_CAPACITYEntry。关于这个INITIAL_CAPACITY为什么是2N次方,这在HashMap里面也是有体现的,这里INITIAL_CAPACITY16那么16-1=15在二进制中就是1111.当他和TheadLocalINITIAL_CAPACITY相与的时候,得到的数绝对是<=INITIAL_CAPACITY.这和threadLocalHashCode%INITIAL_CAPACITY的效果是一样的,但是效率比前者好处很多倍。此时我们已经得到一个下标位置,我们直接new了一个Entry(ThreadLocal,Object),放入该table数组当中,这个时候把tablesize置为1,阈值职位INITIAL_CAPACITY2/3(达到最大长度的2/3的时候会扩容)
set操作的整体流程比较简单,主要看到ThreadLocalMap来自于当前运行的线程即可。
接下来是get操作
/**
 * 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);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
其中setInitialValue的源码:
/**
 * 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;
}

 

我们在这里看到了initialValue()的方法的调用,JDK将其实现为protected的方法,而且默认实现中返回的值为nullJDK希望子类能够覆盖该方法,推荐用匿名内部类的方式来实现。这也就解释了上图中的运行结果中两个线程获得相同的初始值的原因。在没有调用set时的情况下第一次调用get方法时就会调用initialValue()也是如此。setInitialValue()的总体思路和set 方法的思路类似。

get方法的流程是这样的:

  1. 首先获取当前线程
  2. 根据当前线程获取一个Map
  3. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
  4. 如果e不为null,则返回e.value,否则转到5
  5. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKeyfirstValue创建一个新的Map

所以,可以总结一下ThreadLocal的设计思路:

每个Thread维护一个ThreadLocalMap映射表,这个映射表的keyThreadLocal实例本身,value是真正需要存储的Object

这个方案与我们设想的方案大体相反。一些资料认为该设计有如下优势:

  • 这样设计之后每个MapEntry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能。(网上的资料,没有测试性能)。
  • Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

本文来自网易实践者社区,经作者李文帅授权发布。