ThreadLocal 线程本地变量 及 源码分析

Posted roman kickCode

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal 线程本地变量 及 源码分析相关的知识,希望对你有一定的参考价值。

■ ThreadLocal 定义

  • ThreadLocal通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题
  • 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
  • 在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本
  • ThreadLocal 自我认识:可以把 "宾馆" 比作应用程序,每个房间为 "子线程",ThreadLocal 就是为每个 "房间" 提供服务的人员,各自不相互冲突并保持独立
  • JDK1.5引入泛型后,ThreadLocal告别Object时代进入泛型时代
  • 存储线程私有变量的一个容器
  • ThreadLocal不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性

 

■ ThreadLocal 数据结构

 1. 类定义

public class ThreadLocal<T>

   2. 重要的内部元素

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;

   3. 构造器

/** ThreadLocal只提供了一个空的默认构造器,够纯粹 **/
public ThreadLocal() {}

 

■ ThreadLocal 重要方法

 1. 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.
  *     设置当前线程在当前ThreadLocal中的线程局部变量的值
  *     其子类无须重写该方法,只要重写initialValue方法设置初始默认值即可
  * @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();
    //获取当前线程持有的Map
    ThreadLocalMap map = getMap(t);
    //createMap or set directly
    if (map != null)
        //注意 key为this,指的就是当前调用set方法的ThreadLocal对象本身
        map.set(this, value);
    else
        //根据当前线程初始化它的ThreadLocalMap并设置值
        createMap(t, value);
}

 2. 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.
  *     返回当前线程在当前ThreadLocak中所对应的线程局部变量
  *     若当前值不存在,则返回initialValue方法设置的初始默认值
  * @return the current thread‘s value of this thread-local
  */
public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程持有的Map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    //Map为空或值为空,返回默认值
    return setInitialValue();
}

 3. remove() 方法

/**
  * 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 {@code initialValue} method in the current thread.
  *     移除当前线程在当前ThreadLocal中对应的私有变量
  *     当该变量之后被当前线程读取(get),该值会重新被initialValue方法初始化除非这期间被set
  *     这将会导致initialValue方法会被当前线程多次调用
  * @since 1.5 该方法是JDK1.5新增方法
  */
public void remove() {
    //获取当前线程持有的ThreadLocalMap,由此可见get和set中的相关代码也应该合并为一行
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //注意是从ThreadLocalMap移除的当前ThreadLocal对象(即ThreadLocalMap的key)
        m.remove(this);
}

 4. getMap,  createMap 方法

/**
  * Get the map associated with a ThreadLocal. Overridden in
  * InheritableThreadLocal.
  *
  * @param  t the current thread
  * @return the map
  */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}


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

 5. nextHashCode 方法

/**
     * Returns the next hash code.
   */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

   6. initialValue 方法

/**
  * Returns the current thread‘s "initial value" for this thread-local variable. 
  * This method will be invoked the first time a thread accesses the variable
  * with the {@link #get} method, unless the thread previously invoked the {@link #set}
  * method, in which case the <tt>initialValue</tt> method will not be invoked for the thread.
  * Normally, this method is invoked at most once per thread, but it may be invoked again
  * in case of subsequent invocations of {@link #remove} followed by {@link #get}.
  *     返回当前线程在当前ThreadLocal中的初始默认值
  *     第一次get操作会调用该方法,除非之前已经调用了set方法(即已有值)
  *     一般情况下该方法只会被执行一次,但有可能出现多次,比如:
  *         调用remove方法之后调用了get方法
  * <p>This implementation simply returns <tt>null</tt>; if the programmer desires
  * thread-local variables to have an initial value other than <tt>null</tt>, 
  * <tt>ThreadLocal</tt> must be subclassed, and this method overridden.
  * Typically, an anonymous inner class will be used.
  *     该方法默认返回null,可以重写该方法(比如继承或实现一个匿名类)
  * @return the initial value for this thread-local
  */
protected T initialValue() {
    return null;
}
---------------
/** 比如 自定义一个String类型的匿名ThreadLocal**/
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>(){
    @Override
    protected String initialValue() {
        return "I am roman";
    }
};

 

■ ThreadLocalMap - 线程隔离的秘密

  • ThreadLocalMap是一个专门为线程本地变量设计的一个特殊的哈希表
  • ThreadLocalMap的key为ThreadLocal,value即为要保存的变量的值
  • 每个线程都有一个私有的ThreadLocalMap对象,其可以存放多个不同ThreadLocal作为key的键值对
  • ThreadLocalMap采用的是开地址法而不是链表来解决冲突,并要求容量必须是2次幂

  1. 类定义

static class ThreadLocalMap

  2. Entry

/**
  * 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.
  *     它使用主要的引用域作为自身的key(即ThreadLocal对象)
  *     由于Entry继承自WeakReference,而ThreadLocal被WeakReference封装
  *     !!重点:因此Entry的Key才是弱引用(而不是Entry)!!(笔者在内存泄露会进一步阐述)
  *     当调用get方法返回null时,这意味着该key不再被引用,因此该entry将会从数组中移除
  *     弱引用:当JVM在GC时如果发现弱引用就会立即回收
  *     比较有意思的是Entry并没有使用HashMap.Entry的链表结构
  *     感兴趣的读者可先思考ThreadLocalMap是如何处理hash冲突的问题(后面就讲解)
  */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //当ThreadLocal的外部强引用被回收时,ThreadLocalMap的key会变成null
    //注意key是个ThreaLocal对象,但因为key被WeakReference封装,因此才具有弱引用特性
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

  3. 重要的内部元素

 /**
 * The initial capacity -- MUST be a power of two.
 *  容量必须2次幂,服务于Hash算法
 */
private static final int INITIAL_CAPACITY = 16;
/**
 * The table, resized as necessary. table.length MUST always be a power of two.
 *  底层实现还是一个Entry数组
 */
private Entry[] table;
/**
  * The number of entries in the table.
  * 数组已有元素数量
  */
private int size = 0;
/**
  * The next size value at which to resize.
  * 阈值,默认为0
  */
private int threshold; // Default to 0

  4. 构造器

/**
  * 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.
  *     默认构造器,包含一个键值对:一个ThreadLocal类型的key,一个任意类型的value
  *     createMap方法会直接使用该构造器一次性完成ThreadLocalMap的实例化和键值对的存储
  */
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    //计算数组下标 跟HashMap的 index = key.hashCode() & (cap -1) 保持一致(即取模运算优化版) 
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //在数组指定下标处填充数组
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);//默认阈值是 32/3 约等于 10.6667
}
/**
 * Set the resize threshold to maintain at worst a 2/3 load factor.
 *      取len的三分之二,而不是HashMap的0.75
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

  5. ThreadLocalMap 一些重要方法

   1) set 方法

/**
  * Set the value associated with key.
  *     存储键值对,比较有趣的是Entry并不是链表,这意味着ThreadLocalMap底层只是数组
  *     其解决冲突(或者说散列优化)的关键在于神奇的0x61c88647
  *     若遇到过期槽,就占用该过期槽(会涉及位移和槽清除操作)
  *     当清理成功同时到达阈值,需要扩容
  * @param key the thread local object
  * @param value the value to be set
  */
private void set(ThreadLocal key, Object value) {
    Entry[] tab = table;
    int len = tab.length;//数组容量
    //计算数组下标 跟HashMap的 index = key.hashCode() & (cap -1) 保持一致(即取模运算优化版) 
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();
        //若key已存在,替换值即可
        if (k == key) {
            e.value = value;
            return;
        }
        //若当前槽为过期槽,就清除和占用该过期槽
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
        //否则继续往后 直到找到key相等或第一个过期槽为止
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //当清理成功同时到达阈值,需要扩容
    //cleanSomeSlots要处理的量是已有元素数量
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
/**
  * Increment i modulo len. 不超过长度就自增1
  */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

     2) remove 方法

/**
  * Remove the entry for key.
  *     当找到该元素的时候,主要做了两个清洗操作
  *         1.将key(ThreadLocal)设置为null
  *         2.当前槽变成过期槽,因此要清除当前槽所存储的Entry元素(主要是避免内存泄露)
  */
private void remove(ThreadLocal key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();//会将key设为null -> this.referent = null
            expungeStaleEntry(i);//清除过期元素
            return;
        }
    }
}

 

■ 碰撞解决与神奇的0x61c88647

  • 机智的读者肯定发现ThreadLocalMap并没有使用链表或红黑树去解决hash冲突的问题,而仅仅只是使用了数组来维护整个哈希表,那么重中之重的散列性要如何保证就是一个很大的考验
  • ThreadLocalMap 通过结合三个巧妙的设计去解决这个问题:
     1. Entry的key设计成弱引用,因此key随时可能被GC(也就是失效快),尽量多的面对空槽 
        2. (单个ThreadLocal时)当遇到碰撞时,通过线性探测的开放地址法解决冲突问题
        3. (多个ThreadLocal时)引入了神奇的0x61c88647,增强其的散列性,大大减少碰撞几率
  • 之所以不用累加而用该值,笔者认为可能跟其找最近的空槽有关(跳跃查找比自增1查找用来找空槽可能更有效一些,因为有了更多可选择的空间spreading out),同时也跟其良好的散列性有关
/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 *  为了让哈希码能均匀的分布在2的N次方的数组里
 */
private static final int HASH_INCREMENT = 0x61c88647;
/**
 * Returns the next hash code.
 *  每个ThreadLocal的hashCode每次累加HASH_INCREMENT
 */
private static int nextHashCode() {
    //the previous id + our magic number
    return nextHashCode.getAndAdd(HASH_INCREMENT); 
}

 

■ ThreadLocal 的实现机制

  • 每个线程都拥有一个ThreadLocalMap对象,即 ThreadLocal.ThreadLocalMap threadLocals = null
  • 每一个ThreadLocal对象有一个创建时生成唯一的HashCode,即 nextHashCode(),通过取模确定所在槽下标位置
  • 访问一个ThreadLocal变量的值,即是查找ThreadLocalMap中对应键值对,即key为该ThreadLocal的键值对
  • 由于一个ThreadLocalMap可以拥有很多个ThreadLocal,推导可得一个线程可拥有多个ThreadLocal(或者说拥有多个不同ThreadLocal作为key的键值对)
//可以定义多个ThreadLocal,每个线程都拥有自己私有的各种泛型的ThreadLocal
//比如线程A可同时拥有以下三个ThreadLocal对象作为key
ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>();
ThreadLocal<Object> objectThreadLocal = new ThreadLocal<Object>();
ThreadLocal<Integer> intThreadLocal = new ThreadLocal<Integer>();

 

PS:

***** 后续会和大家讨论一些内存泄露与ThreadLocal 的实际应用问题,请多指教!! ************

 

以上是关于ThreadLocal 线程本地变量 及 源码分析的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal定义使用案例及源码分析

ThreadLocal源码分析

(转)Java多线程学习之ThreadLocal源码分析

多线程爬坑之路-ThreadLocal源码及原理的深入分析

ThreadLocal源码及相关问题分析

Envoy源码分析之ThreadLocal