[Java并发]ThreadLocal

1、基础概念

在这里插入图片描述

Stack & Heap

Stack和Heap是我们常说的栈和堆,这里不做赘述,只需要备注一点知识即可,Stack是线程私有独享且线程安全的,Heap是所有线程共享非线程安全的。TheadLocal的设计初衷就是希望让线程拥有了自己内部独享的变量,每个线程之间隔离互不干扰以起到线程安全的目的。

ThreadLocal

  • ThreadLocal是我们所说的线程本地变量。如上图所示,它的内部封装了一个非常重要的数据结构ThreadLocalMap来提供线程变量数据的真实获取、存储及移除等操作。我们可以把ThreadLocal理解称为一个封装类或是一个中介对象,所有的核心方法如get、set、remove等都通过ThreadLocal来提供和交互,而真正的幕后大佬是ThreadLocalMap这个封装了最终方法逻辑和内部数据结构的内部类。
private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    
    
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
  • ThreadLocal内部还持有一个实例变量threadLocalHashCode,它是一个哈希值用来做ThreadLocalMap中Entry哈希表路由计算的,threadLocalHashCode的生成方式是根据HASH_INCREMENT这个哈希魔数进行自加操作,关于哈希算法这部分后面会提到。

ThreadLocalMap

static class ThreadLocalMap {
    
    

    // hash map中的entry继承自弱引用WeakReference,指向threadLocal对象
    // 对于key为null的entry,说明不再需要访问,会从table表中清理掉
    // 这种entry被成为“stale entries”
    static class Entry extends WeakReference<ThreadLocal<?>> {
    
    
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
    
    
            super(k);
            value = v;
        }
    }

    private static final int INITIAL_CAPACITY = 16;

    private Entry[] table;

    private int size = 0;

    private int threshold; // Default to 0

    private void setThreshold(int len) {
    
    
        threshold = len * 2 / 3;
    }

    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是一个自定义的hash map,专门用来保存线程的thread local变量
  • 它的操作仅限于ThreadLocal类中,不对外暴露
  • 这个类被用在Thread类的私有变量threadLocals和inheritableThreadLocals上
  • 为了能够保存大量且存活时间较长的threadLocal实例,hash table entries采用了WeakReferences作为key的类型
  • 一旦hash table运行空间不足时,key为null的entry就会被清理掉

Entry

这里的Entry继承了WeakReference类,它的内部构成是一个键值对结构,Key是弱引用的referant,是从WeakReference对象继承而来的,Value是实际存储的线程变量对象数据,即<K,V>=<Referant,Object>,而这里Entry进行了泛型限制,最终定义为Entry<ThrealLocal,Object>的数据格式

2、源码核心方法解析

get()源码

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();
}
  • 获取当前线程内部的ThreadLocalMap
  • map存在则获取当前ThreadLocal对应的value值
  • map不存在或者找不到value值,则调用setInitialValue,进行初始化

setInitialValue()源码

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方法,获取初始化值【调用者通过覆盖该方法,设置自己的初始化值】
  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

get()方法时序图

avatar

set()源码

public void set(T value) {
    
    
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

remove()源码

public void remove() {
    
    
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
     m.remove(this);
}

remove()方法时序图

avatar

3、核心算法

哈希值生成算法

哈希值生成算法:HASH_INCREMENT是参与哈希计算的哈希魔数,这里是16进制的0x61c88647,转换为10进制就是-1640531527,
每次自增HASH_INCREMENT进行生成,这里使用了AtomicInteger保证了线程安全

索引生成算法

索引生成算法:key.threadLocalHashCode & (len-1)

  • key.threadLocalHashCode即通过HASH_INCREMENT自增1得到的哈希值
  • len即当前哈希槽的容量,初始化默认是16,即时哈希槽扩容也是保持16的2倍,可以理解为2的N次方
  • 综上,索引生成是(HASH_INCREMENT自增HASH_INCREMENT)&(2的N次方-1)

黄金分割数

ThreadLocalMap采用黄金分割数的方式,大大降低了哈希冲突的情况。
avatar

  • 黄金分割数: int的黄金分割数是按照int的最大值2147483647乘以黄金分割比例Math.sqrt(5) - 1) / 2 计算得来的,最终int的黄金分割数为-1640531527。
  • 哈希魔数: 哈希数是通过哈希魔数HASH_INCREMENT加自身进行变更,经过研究发现这样会具有更好的散列性。
  • 高低位:(2的N次方-1)生成的数字均有一个特点,二进制高位为0,低位为1
  • 高效按位与:(HASH_INCREMENT自增HASH_INCREMENT)&(2的N次方-1)是索引生成的算法,因为是按位与运算,且(2的N次方-1)高位为0,低位为1,因此变成了HashCode &(2的N次方-1)= HashCode %(2的N次方-1)的运算,但是按位与运算效率要高于取模运算

(2的N次方-1)的二进制

21次方 	num:1 	             binary:00000000000000000000000000000001 
22次方 	num:3 	             binary:00000000000000000000000000000011 
23次方 	num:7 	             binary:00000000000000000000000000000111 
24次方 	num:15 	             binary:00000000000000000000000000001111 
25次方 	num:31 	             binary:00000000000000000000000000011111 
26次方 	num:63 	             binary:00000000000000000000000000111111 
27次方 	num:127 	         binary:00000000000000000000000001111111 
28次方 	num:255 	         binary:00000000000000000000000011111111 
29次方 	num:511 	         binary:00000000000000000000000111111111 
210次方    num:1023 	         binary:00000000000000000000001111111111 
211次方    num:2047 	         binary:00000000000000000000011111111111 
212次方    num:4095 	         binary:00000000000000000000111111111111 
213次方    num:8191 	         binary:00000000000000000001111111111111 
214次方    num:16383 	         binary:00000000000000000011111111111111 
215次方    num:32767 	         binary:00000000000000000111111111111111 
216次方    num:65535 	         binary:00000000000000001111111111111111 
217次方    num:131071 	         binary:00000000000000011111111111111111 
218次方    num:262143 	         binary:00000000000000111111111111111111 
219次方    num:524287 	         binary:00000000000001111111111111111111 
220次方    num:1048575 	 	 binary:00000000000011111111111111111111 
221次方    num:2097151 	 	 binary:00000000000111111111111111111111 
222次方    num:4194303 	 	 binary:00000000001111111111111111111111 
223次方    num:8388607 	 	 binary:00000000011111111111111111111111 
224次方    num:16777215 	 	 binary:00000000111111111111111111111111 
225次方    num:33554431 	 	 binary:00000001111111111111111111111111 
226次方    num:67108863 	 	 binary:00000011111111111111111111111111 
227次方    num:134217727 	 	 binary:00000111111111111111111111111111 
228次方    num:268435455 	 	 binary:00001111111111111111111111111111 
229次方    num:536870911 	 	 binary:00011111111111111111111111111111 
230次方    num:1073741823 	     binary:00111111111111111111111111111111

不难发现,2的幂次方-1的数字的二进制有一个特点那就是,高位都是0,,低位都是1。

(HASH_INCREMENT自增HASH_INCREMENT)的二进制

id:1 	 hashCode:1640531527 	 binary:01100001110010001000011001000111 
id:2 	 hashCode:-1013904242 	 binary:11000011100100010000110010001110 
id:3 	 hashCode:626627285 	 binary:00100101010110011001001011010101 
id:4 	 hashCode:-2027808484 	 binary:10000111001000100001100100011100 
id:5 	 hashCode:-387276957 	 binary:11101000111010101001111101100011 
id:6 	 hashCode:1253254570 	 binary:01001010101100110010010110101010 
id:7 	 hashCode:-1401181199 	 binary:10101100011110111010101111110001 
id:8 	 hashCode:239350328 	 binary:00001110010001000011001000111000 
id:9 	 hashCode:1879881855 	 binary:01110000000011001011100001111111 
id:10 	 hashCode:-774553914 	 binary:11010001110101010011111011000110 
id:11 	 hashCode:865977613 	 binary:00110011100111011100010100001101 
id:12 	 hashCode:-1788458156 	 binary:10010101011001100100101101010100 
id:13 	 hashCode:-147926629 	 binary:11110111001011101101000110011011 
id:14 	 hashCode:1492604898 	 binary:01011000111101110101011111100010 
id:15 	 hashCode:-1161830871 	 binary:10111010101111111101111000101001 
id:16 	 hashCode:478700656 	 binary:00011100100010000110010001110000 
id:17 	 hashCode:2119232183 	 binary:01111110010100001110101010110111 
id:18 	 hashCode:-535203586 	 binary:11100000000110010111000011111110 
id:19 	 hashCode:1105327941 	 binary:01000001111000011111011101000101 
id:20 	 hashCode:-1549107828 	 binary:10100011101010100111110110001100 
id:21 	 hashCode:91423699 	     binary:00000101011100110000001111010011 
id:22 	 hashCode:1731955226 	 binary:01100111001110111000101000011010 
id:23 	 hashCode:-922480543 	 binary:11001001000001000001000001100001 
id:24 	 hashCode:718050984 	 binary:00101010110011001001011010101000 
id:25 	 hashCode:-1936384785 	 binary:10001100100101010001110011101111 
id:26 	 hashCode:-295853258 	 binary:11101110010111011010001100110110 
id:27 	 hashCode:1344678269 	 binary:01010000001001100010100101111101 
id:28 	 hashCode:-1309757500 	 binary:10110001111011101010111111000100 
id:29 	 hashCode:330774027 	 binary:00010011101101110011011000001011 
id:30 	 hashCode:1971305554 	 binary:01110101011111111011110001010010 
id:31 	 hashCode:-683130215 	 binary:11010111010010000100001010011001

以上通过nextHashCode.getAndAdd(HASH_INCREMENT)生成32次hashCode。
由于(2的N次方-1)的二进制高位均为1,且是&运算,因此hashCode越具有散列性,最终索引值也会具有很好的散列性,哈希碰撞的可能性就会减少。
在(2的N次方-1)的二进制也就是length固定的情况下,低位都是1,高位都是0,因此,hashCode高位或低位相同太多会导致严重碰撞,一定要如上图这样到高低位都能具有很好的差异性参与计算才可以减少碰撞

id:1 	 hashCode:1640531527 	 index:7 
id:2 	 hashCode:-1013904242 	 index:14 
id:3 	 hashCode:626627285 	 index:21 
id:4 	 hashCode:-2027808484 	 index:28 
id:5 	 hashCode:-387276957 	 index:3 
id:6 	 hashCode:1253254570 	 index:10 
id:7 	 hashCode:-1401181199 	 index:17 
id:8 	 hashCode:239350328 	 index:24 
id:9 	 hashCode:1879881855 	 index:31 
id:10 	 hashCode:-774553914 	 index:6 
id:11 	 hashCode:865977613 	 index:13 
id:12 	 hashCode:-1788458156 	 index:20 
id:13 	 hashCode:-147926629 	 index:27 
id:14 	 hashCode:1492604898 	 index:2 
id:15 	 hashCode:-1161830871 	 index:9 
id:16 	 hashCode:478700656 	 index:16 
id:17 	 hashCode:2119232183 	 index:23 
id:18 	 hashCode:-535203586 	 index:30 
id:19 	 hashCode:1105327941 	 index:5 
id:20 	 hashCode:-1549107828 	 index:12 
id:21 	 hashCode:91423699 	     index:19 
id:22 	 hashCode:1731955226 	 index:26 
id:23 	 hashCode:-922480543 	 index:1 
id:24 	 hashCode:718050984 	 index:8 
id:25 	 hashCode:-1936384785 	 index:15 
id:26 	 hashCode:-295853258 	 index:22 
id:27 	 hashCode:1344678269 	 index:29 
id:28 	 hashCode:-1309757500 	 index:4 
id:29 	 hashCode:330774027 	 index:11 
id:30 	 hashCode:1971305554 	 index:18 
id:31 	 hashCode:-683130215 	 index:25 
id:32 	 hashCode:957401312 	 index:0

以上是length为32时,生成的32次索引值的情况,发现索引值分布非常均匀,没有出现碰撞。

哈希冲突解决

当出现哈希冲突时,它的做法看是否是同一个对象或者是是否可以替换,否则往后移动一位,继续判断,这里采用的是再次寻址的方法。

private static int nextIndex(int i, int len) {
    
    
    return ((i + 1 < len) ? i + 1 : 0);
}

内存清理

ThreadLocal之所以采用Entry的key使用弱引用就是为了尽快回收避免大量占用内存空间,除此之外还创造性的增加了探测式清理、启发式清理两种方式在核心方法get、set等调用时进行内存对象的回收和清理工作

探测式清理

private int expungeStaleEntry(int staleSlot) {
    
    
    Entry[] tab = table;
    int len = tab.length;
 
    // 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
    tab[staleSlot].value = null;
    // 显式设置该entry为null,以便垃圾回收
    tab[staleSlot] = null;
    size--;
 
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
    
    
        ThreadLocal<?> k = e.get();
        // 清理对应ThreadLocal已经被回收的entry
        if (k == null) {
    
    
            e.value = null;
            tab[i] = null;
            size--;
        } else {
    
    
            /*
             * 对于还没有被回收的情况,需要做一次rehash。
             * 
             * 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
             * 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
             */
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
    
    
                tab[i] = null;
                
                /*
                 * 在原代码的这里有句注释值得一提,原注释如下:
                 *
                 * Unlike Knuth 6.4 Algorithm R, we must scan until
                 * null because multiple entries could have been stale.
                 *
                 * 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列)
                 * 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。
                 * R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引
                 * 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index,
                 * 继续向后扫描直到遇到空的entry。
                 *
                 * ThreadLocalMap因为使用了弱引用,所以其实每个slot的状态有三种也即
                 * 有效(value未回收),无效(value已回收),空(entry==null)。
                 * 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。
                 *
                 * 因为expungeStaleEntry函数在扫描过程中还会对无效slot清理将之转为空slot,
                 * 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。
                 */
                while (tab[h] != null) {
    
    
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot之后第一个空的slot索引
    return i;
}
  • 根据场景改进了高德纳论述的从使用线性探测的散列表中删除一个元素的R算法
  • 如果index对应的slot就是要读的threadLocal,则直接返回结果
  • 调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
  • 没有找到key,返回null

启发式清理

private boolean cleanSomeSlots(int i, int n) {
    
    
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
    
    
        // i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
    
    
            // 扩大扫描控制因子
            n = len;
            removed = true;
            // 清理一个连续段
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
 }
  • 启发式地清理slot,i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空),n是用于控制控制扫描次数的
  • 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
  • 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理,再从下一个空的slot开始继续扫描
  • 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,区别是前者传入的n为元素个数,后者为table的容量

4、缺点

内存泄露问题

在这里插入图片描述
如上图,整理对象引用关系,用**++>表示强引用,用–>**表示弱引用

  • Thread ++> ThrealLocal.ThreadLocalMap ++> Entry ++> key (referant) --> ThreadLocal
  • Thread ++> ThrealLocal.ThreadLocalMap ++> Entry ++> value ++> Object
    由于key (referant) --> ThreadLocal是弱引用,gc时会回收key,此时key为null,但是Thread作为这条引用链的Root根不会立刻线程执行完毕而消失,会一直驻留在Stack中,这种情况一般可以有两种,一个事在一个for循环中执行,一种是线程池中执行,因此会导致强引用的value不会进行释放对象导致内存溢出

父子线程无法传递线程副本数据

在主线程中使用ThreadLocal无法直接传递给子线程,如果要操作还需要通过线程封闭进行变量置换。

5、问题汇总

  • ThreadLocal的key是弱引用,那么在 threadLocal.get()的时候,发生GC之后,key是否为null?
    由于Entry<WeakReference,Object>的key是弱引用,GC后就会被回收
  • ThreadLocal中ThreadLocalMap的数据结构?
    哈希表
  • ThreadLocalMap的Hash算法?
    key.threadLocalHashCode & (length -1) , length为2的幂次方
  • ThreadLocalMap中Hash冲突如何解决?
    开放地址,二次寻址,由于使用黄金分割数进行哈希计算,散列非常好,出现碰撞的可能性很低,所以没有像HashMap那样进行链地址解决冲突
  • ThreadLocalMap扩容机制?
    length2/3 触发rehash逻辑,进行探测式清理,最终判断size >= threshold 3/4来决定是否要真正扩容调用resize方法
  • ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程?
    探测式清理(expungeStaleEntry())、启发式清理(cleanSomeSlots())
    探测式清理是以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理,结合了
  • ThreadLocalMap.set()方法实现原理?
  • ThreadLocalMap.get()方法实现原理?
  • 项目中ThreadLocal使用情况?遇到的坑?
    父子线程不能传递线程变量,主线程中使用线程池相当于父线程中使用子线程无法传值

6、实战应用

复杂场景

Spring容器、RPC全链路traceId传递等

应用举例

在单体应用中一般不会声明多个ThreadLocal,即不会让Thread中持有的ThreadLocalMap的key有多个,由于ThreadLocal是支持泛型的,我们可以传入一个线程安全的容器,让Thread内持有的ThreadLocalMap中只有一个key,即只有一个ThreadLocal的key引用,而存储的Object可以是一个Map或者List,我们根据业务场景操作容器即可,大部分情况都可以满足,设计合理的话是可以共用的,减少持有key也不会占用大量的栈空间,且把ThreadLocal声明为private final static,下面提供一个demo:

/**
 * @author: guanjian
 * @date: 2020/07/08 9:31
 * @description: 环境变量
 */
@Component("contextHolder")
public class ContextHolder<T, R> {
    
    

    private final static Logger LOGGER = LoggerFactory.getLogger(ContextHolder.class);
    /**
     * 入参对象
     */
    public final static String REQUEST_PARAM = "request_param";

    /**
     * 出参对象
     */
    public final static String RESPONSE_PARAM = "response_param";

    /**
     * 传值对象
     */
    public final static String TRANSMIT_PARAM = "transmit_param";

    /**
     * 线程变量
     */
    private final static ThreadLocal<Map<Object, Object>> localVariable = ThreadLocal.withInitial(() -> Maps.newHashMap());

    public void bindLocal(Object key, Object value) {
    
    
        Objects.requireNonNull(key, "key can not be null");

        Map holder = localVariable.get();

        holder.put(key, value);

        localVariable.set(holder);

        LOGGER.debug("[ContextHolder] key={},value={} binded.", key, JSON.toJSONString(value));
    }

    public Object getLocal(Object key) {
    
    
        if (CollectionUtils.isEmpty(localVariable.get())) return null;

        Object value = localVariable.get().get(key);

        LOGGER.debug("[ContextHolder] key={},value={} getted.", key, JSON.toJSONString(value));
        return value;
    }

    public void bindRequest(T value) {
    
    
        bindLocal(REQUEST_PARAM, value);
    }

    public T getRequest() {
    
    
        return (T) localVariable.get().get(REQUEST_PARAM);
    }

    public void bindResponse(R value) {
    
    
        bindLocal(RESPONSE_PARAM, value);
    }

    public R getResponse() {
    
    
        return (R) localVariable.get().get(RESPONSE_PARAM);
    }

    public void bindTransmit(Object value) {
    
    
        bindLocal(TRANSMIT_PARAM, value);
    }

    public Object getTransmit() {
    
    
        return getLocal(TRANSMIT_PARAM);
    }

    public void clear() {
    
    
        localVariable.remove();
    }
}

7、参考

https://www.cnblogs.com/wang-meng/p/12856648.html
https://blog.csdn.net/zjcsuct/article/details/104310194
http://www.iocoder.cn/JDK/ThreadLocal/
https://blog.csdn.net/qq_22167989/article/details/89448670

猜你喜欢

转载自blog.csdn.net/u013161278/article/details/109510504