ThreadLocal详解

一、概括

ThreadLocal提供线程局部变量。这些变量不同于它们的正常对应变量,因为每一个访问(通过它的 get 或 set 方法)的线程都有它自己的独立初始化变量副本。 ThreadLocal 实例通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户 ID 或事务 ID)。
  只要线程处于活动状态并且ThreadLocal 实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在线程消失后,它的所有线程本地实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)。

二、三要素

学习ThreadLocal需要清楚Thread、ThreadLocalMap、ThreadLocal三者之间的关系:当前Thread以独有的ThreadLocal为key去ThreadLocalMap获取独有的局部变量
引用一个网图:
在这里插入图片描述
具体可根据下面描述的原理来进行理解。

三、原理(结合源码分析)

Thread中有成员变量threadLocals,就是ThreadLocalMap类型的对象,ThreadLocalMap是ThreadLocal的内部。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

看ThreadLocalMap的源码,发现Entry底层实现如下,每个 Entry 键值对以ThreadLocal为key,Object为value,这是一个弱引用(WeakReference)

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

ThreadLocalMap类提供了set() 方来设置和添加键值对, 提供了get()方法来获取键值对,提供了remove()方法来删除键值对,这几个方法跟ThreadLocal很相似。

下面开始看ThreadLocal中的主要方法:

	/**
     * 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.
     *
     * @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();//获取当前线程对象
        ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap对象
        if (map != null)//获取到了当前线程对应的ThreadLocalMap对象
            map.set(this, value);//以ThreadLocal对象为键,要存储的数据对象为值,存入当前线程的ThreadLocalMap对象中
        else
            createMap(t, value);//没有获取到就创建下
    }

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {//获取当前线程对应的ThreadLocalMap对象
        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);//this指的是当前ThreadLocal对象;firstValue指的是要存储在此线程本地的当前线程副本中的值。
    }

解析set()方法:
  调用ThreadLcoal对象的set()方法时, ThreadLocal对象会获取到当前当前线程的引用,根据这个引用获取到线程的成员ThreadLocalMap对象,然后后调用ThreadLocalMap对象的set方法存储到这个Map中。看似我们是把数据存储在了ThreadLcoal对象中,但是实际上我们是把数据存储在当前线程的ThreadLocalMap中。而threadlocal只是用来在线程中查找这个对象而已。

	/**
     * 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.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();//获取当前线程对象
        ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap对象
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//从ThreadLocalMap中取出以当前ThreadLocal对象为键的键值对
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;//若成功取出,则直接返回这个键值对的值
                return result;
            }
        }
        return setInitialValue();//如果查找键值对失败,则调用这个方法重新在ThreadLocalMap中添加这个数据对象,然后返回值
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {//set() 的变体,用于建立初始值。如果用户重写了 set() 方法,则使用它代替 set()。
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

解析get()方法:
  调用ThreadLcoal对象的get()方法也是类似,先获取当前线程对象引用,然后获取这个线程的成员对象ThreadLocalMap,以 ThreadLocal 引用为键,取出这个键值对中的值。

	/**
     * 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.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程对应的ThreadLocalMap对象
         if (m != null)
             m.remove(this);//移除ThreadMap中以当前ThreadLocal对象为键的键值对
     }

解析remove():
  调用ThreadLcoal对象的remove方法也是先获取当前线程对象引用,然后获取这个线程的成员对象ThreadLocalMap,最后移除以 ThreadLocal 引用为键的键值对。

原理解析:
  ThreadLocal 的set(), get(), remove()方法实际上在操作当前线程成员变量 threadlocals, 这个变量的类型是一个ThreadLocalMap(以 ThreadLocal 引用为键,对象为值)。一切操作都是在操作当前线程中的值,threadlocal在这里只是相当于一个索引作用。那么对 ThreadLocal 中存储的对象进行操作当然就是线程安全的了,因为始终都是操作的当前线程,不涉及到其他线程,当然就不会线程不安全了。
  因为每个健值在ThreadMap中是唯一的,它唯一标识了一个健值对,所以我们在ThreadLocalMap中不能存储多个健值相等的键值对,而因为这个ThreadLocalMap是以ThreadLocal对象引用为健值,所以一个ThreadLocalMap对象只能存储一个以同一个ThreadLocal对象引用为键的键值对,也就是每个线程对同一个ThreadLocal对象,只能存储一个数据对象。

四、ThreadLocal的内存泄漏问题

1、问题如下:
  在ThreadLocalMap中,entry的key是弱引用,value仍然是一个强引用。当某一条线程中的ThreadLocal使用完毕,没有强引用指向它的时候,这个key指向的对象就会被垃圾收集器回收,从而这个key就变成了null;所以entry就变成了(null, value), 而entry 和 value 都是强引用,并且只要entry还在,value就一直存在。
  即一直存在一条引用链: Thread -> ThreadLocalMap -> Entry -> Value
  所以如果我们不手动清理掉这些键为空的entry, 在线程执行完毕之前,这个entry就一直处于内存泄漏的状态。
  线程越多,线程生命周期越长,内存泄漏的就越多。

源码解析内存泄漏问题:

static class Entry extends WeakReference<ThreadLocal<?>> {
	/** The value associated with this ThreadLocal. */
	Object value;
	
	Entry(ThreadLocal<?> k, Object v) {
		super(k);//Entry对象中的key使用了WeakReference封装,即Entry中的key是一个弱引用类型,对于弱引用来说,它只能存活到下次GC之前
		value = v;//Entry对象中的value是强引用类型,若线程一直存在的话,那么它的 value值就会一直存在
	}
}
public class WeakReference<T> extends Reference<T> {

    /**
     * 创建一个引用给定对象的新弱引用。新引用未在任何队列中注册。
     *
     * @param 新的弱引用将引用的引用对象
     */
    public WeakReference(T referent) {
        super(referent);
    }
}
public abstract class Reference<T> {
	Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}

2、如何解决:
  每次操作set、get、remove操作时,会相应调用ThreadLocalMap的三个方法,ThreadLocalMap的三个方法在每次被调用时都会直接或间接调用一个expungeStaleEntry()方法,这个方法会将key为null的Entry删除,从而避免内存泄漏
  如果一个线程运行周期较长,而且将一个大对象放入LocalThreadMap后便不再调用set、get、remove方法仍然有可能key的弱引用被回收后,引用没有被回收,此时该仍然可能会导致内存泄漏。
  这个问题确实存在,没办法通过ThreadLocal解决,而是需要程序员在完成ThreadLocal的使用后要养成手动调用remove的习惯,从而避免内存泄漏

3、既然弱引用会导致内存泄漏,那ThreadLocalMap为什么对ThreadLocal的引用要设置成弱引用?
  为了尽快回收这个线程变量,因为这个线程变量可能使用场景不是特别多,所以希望使用完后能尽快被释放掉。因为线程拥有的资源越多,就越臃肿,线程切换的开销就越大,所以希望尽量降低线程拥有的资源量。

附录:

引用类型功能特点
强引用 ( Strong Reference )被强引用关联的对象永远不会被垃圾回收器回收掉
软引用( Soft Reference )软引用关联的对象,只有当系统将要发生内存溢出时,才会去回收软引用引用的对象
弱引用 ( Weak Reference )只被弱引用关联的对象,只要发生垃圾收集事件,就会被回收
虚引用 ( Phantom Reference )被虚引用关联的对象的唯一作用是能在这个对象被回收器回收时收到一个系统通知
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值