深入理解ThreadLocal

本文详细探讨了ThreadLocal的工作原理,包括其如何与线程绑定,存储结构以及解决哈希冲突的方法。文章指出,ThreadLocal通过ThreadLocalMap存储线程局部变量,使用弱引用避免内存泄漏,但在多线程环境下,直接使用ThreadLocal无法在父线程和子线程间传递值。为解决此问题,文章提到了InheritableThreadLocal和TransmittableThreadLocal的区别与应用。

提出问题:

  1. ThreadLocal 是如何和线程绑定的,ThreadLocal 的存储结构,如何解决hash冲突的
  2. ThreadLocal是否会导致内存泄漏,为啥key要使用WeakReference
  3. 在一个父线程开启多个子线程的情况下如何使用ThreadLocal

下面我们一个一个问题来
1. ThreadLocal 的存储结构,如何解决hash冲突的

在这里插入图片描述
ThreadLocal 是如何和线程绑定的:
上面的图简单描述他们的调用关系,简单来说就是当我们用ThreadLocal设置值得时候,

  1. 会先获取当前线程,然后取得线程里面的ThreadLocalMap,没有就重新建一个,
  2. ThreadLocalMapHashMap差不多。里面有个Entry[] table,
  3. Entrykey值是ThreadLocal本身,value值是要设置的值。

通过ThreadLocal获取值得时候,也是先获取当前线程,然后取得线程里面的ThreadLocalMap。所以每个线程都使用的是自己线程里面的ThreadLocalMap, 同一个线程new的多个ThreadLocal只会创建一个ThreadLocalMap, 实现了线程隔离。get, 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);
    }
    
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 getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocal 的存储结构

  1. 一个ThreadLocal包含一个ThreadLocalMap
  2. 一个ThreadLocalMap包含一个Entry[],
  3. Entry包含一个key值一个value值,key值就是ThreadLocal本生,value值为设置的值,

同一个线程如果new了多个ThreadLocal,但是只会创建一个ThreadLocalMap,所有的值都保存在同一个Entry[], 这时候就可能存在hash冲突,hash冲突简单的解释下就是两个不同的key值通过hash计算后获得的值是一样的

如何解决hash冲突?
线性探测法 : 讲的通俗一点,就是发现蹲坑的时候发现坑已经被占了,就找后面一个坑,如果后面一个坑空闲,则占用这个空闲的坑;如果后面一个坑也被占了,则一直往后面的坑进行遍历,直到找到空闲的坑,否则就一直憋着。 查找的时候,也是先通过hash计算出位置,然后判断key是否相等,相等就获取value值,不相等就查找后面一位,直到找到相等的。 ThreadLocalMap的set代码如下

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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)]) {
                ThreadLocal<?> k = e.get();
                //如果key值相等就更新
                if (k == key) {
                    e.value = value;
                    return;
                }
                //k为null,也就是Entry的key已经被回收了,当前的Entry是一个陈旧的元素(stale entry)
                if (k == null) {
                    // 用新元素替换掉陈旧元素,同时也会清理其他陈旧元素,防止内存泄露
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            //清理key值为null的元素,同时判断是否需要扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

2. ThreadLocal是否会导致内存泄漏,为啥key要使用WeakReference

内存泄漏是啥意思, 简单来栈内存里面的引用被清楚了,但是堆里面的数据无法回收,但是程序里面里面又用不了这个对象,就叫内存泄漏,大量的内存泄漏会导致内存溢出。

我们看下Entry的代码

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

ThreadLocal<?>也就是key值使用了弱引用,弱引用简单来说就是垃圾回收器发现一个对象上只有弱引用,此时不管内存够不够,这个对象都会被回收。
Entry 为啥要使用WeakReference?
我的理解:

  1. 如果我们不需要使用这个ThreadLocal,
  2. 然后将ThreadLocal设置为null,栈和对内存的ThreadLocal 被清除,
  3. 但是多个ThreadLocal同用一个的ThreadLocalMap并不会被清除,如果都是强引用,ThreadLocalMap里面这个被清除的ThreadLocal的Entry也无法被清除掉。

但是如果Entry弱引用了ThreadLocal,如果ThreadLocal被清除掉,这个弱引用的ThreadLocal就没有任何强引用,key值也将被垃圾回收器回收为null, 然后ThreadLocalMap在get,set, remove方法中会调用cleanSomeSlots(i, sz)来清理掉key值为null的元素。

  // expunge entry at staleSlot
 tab[staleSlot].value = null;
 tab[staleSlot] = null;
 size--;

结论:
ThreadLocal大概率是不会产生内存泄漏的,除非你将一个ThreadLocal设置为null后,在也没有调用ThreadLocalMap里面的get,set, remove, 那么这个ThreadLocal里面的Entry的value值将无法回收,但是为了保险起见还是建议不用这个value值之后使用ThreadLocal.remove() 方法移除掉这个值。最好放在finally里面,如下

try {
            ThreadLocalCache.TEST.set("XXXX");
             ........
        } finally {
            ThreadLocalCache.TEST.remove();
        }

3. 在一个父线程开启多个子线程的情况下如何使用ThreadLocal
在一个父线程开启多个子线程的情况下如何使用ThreadLocal,子线程是获取不到父线程设置的值得,因为子线程会自己new一个ThreadLocalMap, 这个ThreadLocalMap里面并没有父线程ThreadLocalMap的值,基于这种情况,java提供了一个InheritableThreadLocal。InheritableThreadLocal的代码很简单

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

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

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

它继承了ThreadLocal, 在getMap的时候返回了Thread里面的inheritableThreadLocals, 这个变量在线程里面的定义是使用如下, 简化了下不需要的代码,

public
class Thread implements Runnable {
 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {

        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
   }
}

简单来说就是在线程初始化的时候判断父类有没有inheritableThreadLocals, 如果有就把父类的inheritableThreadLocals复制给子类。获取值得时候就获取了有值得inheritableThreadLocals。

但是真实的项目中一般不会自己去new Thread,而是通过线程池来创建Thread,这个时候就涉及线程的复用,因为父线程的值传给子线程是在创建子线程的时候copy的,如果重用子线程,就会导致子类中的值不对,可能为空,可能为上个父线程的值,这个时候就要 使用alibaba提供的TransmittableThreadLocal

 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.2.0</version>
        </dependency>

使用

 public static final ThreadLocal<Integer> test = new TransmittableThreadLocal<>();
  test.set(1111);
  test.get();
  //Executor需要用TtlExecutors包一下
    public Executor  executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setThreadNamePrefix("collectorExecutor-");
        executor.initialize();
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return TtlExecutors.getTtlExecutor(executor);
 }

里面的具体实现逻辑,等我看懂了在来分享吧。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值