reupe

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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对象也不会内存泄漏了。

posted on 2020-01-05 20:27  yxlaisj  阅读(362)  评论(0编辑  收藏  举报