• ThreadLocal的学习


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

  • 相关阅读:
    GCC默认的标准不是ANSI C,而是GNU C90
    C/C++预定义宏
    GCC对C标准的支持
    Tupper's selfreferential formula
    VC++对C标准的支持
    一道笔试题
    C语言标准
    FLVPlayback视频
    getDefinitionByName与ApplicationDomain.getDefinition
    SVN
  • 原文地址:https://www.cnblogs.com/yxlaisj/p/12150103.html
Copyright © 2020-2023  润新知