1. 为什么使用ThreadLocal?
假设我们开发某平台服务,提供一系列接口,客户访问时需要必须传入user信息,服务端最好是将user信息保存为全局变量,一边在处理某一次请求的过程中能够随时获取,而无需费力进行参数的透传。对于服务端而言,每一个请求,就是一个线程,而每个请求传来的user信息,也就应该只属于这个线程,那么有没有什么方法,能把user信息与当前线程绑定呢?ThreadLocal就提供了这样的方法。
2. 一个例子
根据java.lang.ThreadLocal类的注释,通常使用ThreadLocal,使用private static来修饰,ThreadLocal被定义为一个静态变量,所以,多个线程其实会共用同一个ThreadLocal对象,但每一个线程都会持有ThreadLocal的副本,所以并不会造成资源共享上的问题,具体原因会在下一节的实现原理中解释。下面的代码是是使用TheadLocal的一个例子,例子出自《thinking in java》
1 public class ThreadLocalVariableHolder { 2 private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){ // ThreadLocal对象“持有”Integer变量 3 private Random rand = new Random(); 4 protected Integer initialValue() { // initalValue方法,会给ThreadLocal的Integer变量设置一个初始值 5 return rand.nextInt(10000); // 随机设置一个初始值 6 } 7 }; 8 public static void increment() { 9 value.set(value.get() + 1); // ThreadLocal“持有”的Integer值增1 10 } 11 12 public static int get() { 13 return value.get(); // 获取ThreadLocal“持有”的Integer 14 } 15 16 public static void main(String[] args) throws InterruptedException { 17 ExecutorService exec = Executors.newCachedThreadPool(); 18 for (int i = 0; i < 5; i++) { 19 exec.execute(new Accessor(i)); 20 } 21 TimeUnit.MILLISECONDS.sleep(3); 22 exec.shutdownNow(); 23 } 24 } 25 26 class Accessor implements Runnable { // 开线程去跑,每个线程会去调用 ThreadLocalVariableHolder.increment()方法递增ThreadLocal中的Integer值 27 private final int id; 28 public Accessor(int idn) { 29 id = idn; 30 } 31 @Override 32 public void run() { 33 while (!Thread.currentThread().isInterrupted()) { 34 ThreadLocalVariableHolder.increment(); 35 System.out.println(this); 36 Thread.yield(); 37 } 38 } 39 40 public String toString() { // 打印出当前线程对应的Integer值 41 return "#" + id + ": " + ThreadLocalVariableHolder.get(); 42 } 43 }
上述程序开了5个线程,每个线程都会去递增ThreadLocal中的Integer值,值得注意的是,ThreadLocal是static变量,按照一般地理解,这5个线程应该是去递增同一个ThreadLocal中的Integer值吧?实际并不是,从结果中也可以印证
,看起来是每个线程都各自独立存有一份Integer变量并且是相互隔离的,这在下节将会分析。
#0: 2396 #2: 2864 #1: 1717 #2: 2865 #1: 1718 #3: 3218 #0: 2397 #3: 3219 #1: 1719 #2: 2866 #4: 4599 #2: 2867 #1: 1720 #3: 3220 #0: 2398 #3: 3221 #1: 1721 #2: 2868 #4: 4600
3.ThreadLocal的实现原理
先抛开ThreadLocal不谈,在Java中,一切皆为对象,所以线程也是对象,设想一下,如果要做到变量的线程隔离,那么就应该是在每个线程对象中,分别保存这个变量在当前线程中的值,可以用下图来描述: 两个线程共用变量i,i在两个线程中的值不同。每个线程保存一份共享变量i的副本,直接把i和thread对象关联是无法实现的,这就需要借助于ThreadLocal对象了。
ThreadLocal<>实现了泛型,其泛型参数就是程序中想要线程隔离的变量的类型,比如上图中的变量i,把变量i用ThreadLocal<Integer>封装起来,声明为
private static TheadLocal<Integer> value = new ThreadLocal<>();
文档中建议使用private static来声明ThreadLocal。ThreadLocal提供了get()和set()方法(参考上面的例子)来设置和获取变量值。如果要实现线程隔离,那么按照上图的模型,每个Thread对象都应该持有一份此ThreadLocal对象的副本,上图模型修改为下图所示:
设想一下,一次会话中可能会有多处声明了ThreadLocal来封装多个需要隔离的变量,那么Thread对象中就应该持有多个ThreaLocal对象,上图的模型进一步改进为:
线程中,每个ThreadLocal对象都对应着一个相应变量的值,这里我们自然而然的会想到,既然有多个ThreadLocal对象,那么应该使用集合来将其容纳。如果使用List的话,List中的每个元素都是一个TheadLocal对象,这样,就需要将ThreadLocal对象相对应的变量保存在ThreadLocal对象中,ThreadLocal对象中就需要有属性来持有这些变量的值,那么对于同一个ThreadLocal对象,不同的线程中的ThreadLocal应该保存不同的值,显然将值直接封装入ThreadLocal中很难实现。实际上,Java的设计中,Thead对象持有一个映射表的引用,ThreadLocal对象作为映射表的key,而隔离变量的值作为映射表的值。打开Thread类的源码,可以看到一个 ThreadLocal.ThreadLocalMap。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap是不同于HashMap的映射表,模型进一步更改为如下图所示:
上图就是最终的模型,使用这种模型,就不存在共享资源线程安全的问题,每个线程资源之间相互隔离,对于存储会话的session或者上下文信息非常适用。
4.ThreadLocal源码分析
本节主要分析ThreadLocal的核心代码实现。首先浏览类和成员变量的注释,类的注释的大致意思实际就是上节我们分析的内容。注意到几个关于HashCode的属性:
1 public class ThreadLocal<T> { 2 /** 3 * ThreadLocals rely on per-thread linear-probe hash maps attached 4 * to each thread (Thread.threadLocals and 5 * inheritableThreadLocals). The ThreadLocal objects act as keys, 6 * searched via threadLocalHashCode. This is a custom hash code 7 * (useful only within ThreadLocalMaps) that eliminates collisions 8 * in the common case where consecutively constructed ThreadLocals 9 * are used by the same threads, while remaining well-behaved in 10 * less common cases. 11 */ 12 private final int threadLocalHashCode = nextHashCode(); 13 14 /** 15 * The next hash code to be given out. Updated atomically. Starts at 16 * zero. 17 */ 18 private static AtomicInteger nextHashCode = 19 new AtomicInteger(); 20 21 /** 22 * The difference between successively generated hash codes - turns 23 * implicit sequential thread-local IDs into near-optimally spread 24 * multiplicative hash values for power-of-two-sized tables. 25 */ 26 private static final int HASH_INCREMENT = 0x61c88647; 27 28 /** 29 * Returns the next hash code. 30 */ 31 private static int nextHashCode() { 32 return nextHashCode.getAndAdd(HASH_INCREMENT); 33 } 34 35 ... 36 }
上节我们知道,ThreadLocal对象是作为ThreadLocalMap中的key,根据这个key来检索对应的值,而根据什么检索?就是对于同一个线程,每个ThreadLocal都有一个唯一的ID,即threadLocalHashCode,通过这个值就可以检索映射表从而找到相应的值。上面代码无非就是告诉怎么生成HashCode,可以总结如下:
1)每实例化一个ThreadLocal,都调用nextHashCode生成下一个HashCode;
2)nextHashCode调用nextHashCode生成下一个HashCode;
3) nextHashCode是AtomicInteger的实例,它的操作是原子的,并且被定义为static的,即在ThreadLocal第一次被使用的时候就会实例化nextHashCode对象,值从0开始;
4)为什么使用0x61c88647?这是因为ThreadLocalMap中的数组大小定义为2的幂,而0x61c88647这个数可以使ThreadLocal对象均匀地分布在2的幂次方大小的数组中,至于为什么不在本文的讨论范围。
第二节的例子中,实例化ThreadLocal的时候,重写ThreadLocal.InitialValue,这个方法是在ThreadLocal.get()中调用的,如果没有值,就会获取你自定义的初始值。
1 protected T initialValue() { 2 return null; 3 }
看set方法
1 /** 2 * Sets the current thread's copy of this thread-local variable 3 * to the specified value. Most subclasses will have no need to 4 * override this method, relying solely on the {@link #initialValue} 5 * method to set the values of thread-locals. 6 * 7 * @param value the value to be stored in the current thread's copy of 8 * this thread-local. 9 */ 10 public void set(T value) { 11 Thread t = Thread.currentThread(); 12 ThreadLocalMap map = getMap(t); 13 if (map != null) 14 map.set(this, value); 15 else 16 createMap(t, value); 17 }
使用当前线程t去获取其持有的ThreadLocalMap引用,如果为null,说明此线程还没有指向ThreadLocalMap,就去创建一个;如果不为null,就更新当前ThreadLocal对应的值。首先看一下createMap(t,value)方法
1 void createMap(Thread t, T firstValue) { 2 t.threadLocals = new ThreadLocalMap(this, firstValue); 3 }
/** * 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); }
就是创建一个新的ThreadLocalMap对象,这个映射表内部维护一个数组table,元素是Entry对象,和HashMap类似,Entry对象封装key(ThreadLocal)和value(实际值),数组的初始大小是16。
实际上,调用ThreadLocal.get()或者ThreadLocal.set()等方法,就是调用ThreadLocalMap.get()以及ThreadLocalMap.set()等方法,更新值时使用的set方法,会调用ThreadLocalMap.set()方法,源码如下:
1 /** 2 * Set the value associated with key. 3 * 4 * @param key the thread local object 5 * @param value the value to be set 6 */ 7 private void set(ThreadLocal<?> key, Object value) { 8 9 // We don't use a fast path as with get() because it is at 10 // least as common to use set() to create new entries as 11 // it is to replace existing ones, in which case, a fast 12 // path would fail more often than not. 13 14 Entry[] tab = table; 15 int len = tab.length; 16 int i = key.threadLocalHashCode & (len-1); // 求索引 17 18 for (Entry e = tab[i]; 19 e != null; 20 e = tab[i = nextIndex(i, len)]) { // 遍历 21 ThreadLocal<?> k = e.get(); 22 23 if (k == key) { 24 e.value = value; 25 return; 26 } 27 28 if (k == null) { 29 replaceStaleEntry(key, value, i); 30 return; 31 } 32 } 33 34 tab[i] = new Entry(key, value); 35 int sz = ++size; 36 if (!cleanSomeSlots(i, sz) && sz >= threshold) 37 rehash(); 38 }
以上方法就是根据key,找到对应的Entry,更新Entry的值。
ThreadLocal.get()方法同理,也是调用ThreadLoclMap.get()
1 /** 2 * Returns the value in the current thread's copy of this 3 * thread-local variable. If the variable has no value for the 4 * current thread, it is first initialized to the value returned 5 * by an invocation of the {@link #initialValue} method. 6 * 7 * @return the current thread's value of this thread-local 8 */ 9 public T get() { 10 Thread t = Thread.currentThread(); 11 ThreadLocalMap map = getMap(t); 12 if (map != null) { 13 ThreadLocalMap.Entry e = map.getEntry(this); 14 if (e != null) { 15 @SuppressWarnings("unchecked") 16 T result = (T)e.value; 17 return result; 18 } 19 } 20 return setInitialValue(); 21 }
注意到,get和set方法都用到了Entry对象,Entry的定义如下:
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; } } .... }
可以看到Entry继承了WeakReference,entry的key也就是ThreadLocal对象的引用,作为一个弱引用(super(k)),Entry中持有实际值的强引用value。那么这里为什么使用弱引用来指向ThreadLocal对象呢?Java中的解释是:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
意思是,为了应对非常大和长生命周期的引用,哈希表使用key的弱引用。怎么理解呢?首先我们看一下下图的引用关系,虚线表示是弱引用,弱引用在Java中是一种有别于我们一般使用的强引用的引用,弱引用不影响指向对象的生命周期,即如果除了弱引用之外的指向ThreadLocal的强引用都被解除后,那么这个ThreadLocal对象就可以被GC掉。从下图看,就是如果ThreadLocalRef 指向的ThreadLocl的引用断掉后,ThreadLocal对象就可以被GC了,即使这时候还有Key这个弱引用。其实这样做是为了减少内存泄漏发生的几率,假如key使用强引用,存在着CurrentThreadRef -> CurrentThread -> Map -> Key -> ThreadLocal这样一条引用链,那么当ThreadLocalRef被销毁后,实际上程序的意图就是不再使用ThreadLocal了,而我们知道一个线程的生命周期有可能很长,即CurrentThreadRef -> CurrentThread -> Map -> Key -> ThreadLocal这条引用链可能会存在很长时间,这样就造成这样一个结果:明明不再使用ThreadLocal了,但是它无法被GC掉,除非我们去手动将key引用设置为null,这样的话,不如将key设置为弱引用来的保险,至少能保证ThreadLocal对象不会内存泄漏。
但是key设置为弱引用,即便key指向的ThreadLocal被回收了,get(key)返回的就是null。但是由于Entry中的value是强引用,与这个key配对的value并不能被回收,造成value对象内存泄漏了。Java中,称这种key为null的Entry为stale slots. 我们在上面ThreadLocalMap.set(0方法中已经看到了这段代码:
if (k == null) { replaceStaleEntry(key, value, i); return; }
意思是: 如果k为null,说明当前Entry的key指向的ThreadLocl被回收了,这时候传来的key是新的ThreadLocal(拥有相同的索引),这时候,就用新的Entry代替原来的Entry,此外该方法还做了一些垃圾清理的工作,比如之前的key为null但是value不为null造成value无法被清除。实际上,ThreadLocalMap的get(),set(),remove()等方法对key为nullvalue不为null的Entry做了清理工作以防止内存泄漏。为了确实避免内存泄漏,建议在需要销毁线程私有变量的时候,手动调用ThreadLocal.remove()方法,这样就会保证value对象也不会内存泄漏了。