ThreadLocal原理解析以及是否需要调用remove方法

平常的开发过程中,如果有个类不是线程安全的,比如SimpleDateFormat,要使这个类在并发的过程中是线程安全的,那么可以将变量设置位局部变量,不过存在的问题就是频繁的创建对象,对性能和资源会有一定降低和消耗;那么这里就可以用到ThreadLocal作为线程隔离,那么ThreadLocal是如何实现线程与线程之间隔离的呢,待会儿会在下文进行讲解。

之前有篇使用ThreadLocal做线程之间的隔离实例,大家可以参考一下:使用ThreadLocal实现Mybatis多数据源

JDK引用类型

在了解ThreadLocal原理之前,先让大家了解一下JDK中的引用类型。

JDK共有四种引用类型:

1、强引用类型:就是平常创建的对象都属于强引用类型,比如 Object object = new Object();该object为强引用类型,如果该引用没有主动置为null,那么该引用的对象就不会被GC回收,所以一般在写完一段业务之后都会将用到的对象引用置为null,就是为了辅助GC更好的进行垃圾回收。

2、软引用类型:比强引用的类型弱一点,在应用程序发生OOM(内存溢出)之前就会去回收这些弱引用占用的内存,使用的SoftReference类,使用示例如下:

 3、弱引用类型:比软引用类型还要弱一点,在下一次发生GC回收之前就会被垃圾回收器进行回收,使用WeakReference类,使用示例如下:

4、虚引用类型:这个是JDK中最弱的引用类型,在对象被回收之前会移入到一个队列当中,然后在进行删除,这个引用类型用的不多,在JDK中引用的类是PhantomReference,这个需结合引用队列(ReferenceQueue)以及重写finalize方法进行使用,使用示例如下:

 

 对于应用场景来说,如果当前对象为可有可无的话,那么可以使用软引用或者弱引用进行使用,而对应虚引用的话,个人认为可以适用在非GC回收的区域(比如:元空间MetaSpace)使用,可以用来监测这些区域的回收情况等。

基本原理

ThreadLocal的用法呢,这里就先不谈;底层使用的是ThreadLocalMap这个Map的底层采用的是ThreadLocalMap.Entry,ThreadLocalMap这个类在每个线程当中都会存在一份对应的单独得对象,在Thread类中变量如下:

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

这里就是线程安全的重点,因为并发的情况下,每个线程都对应着有份自己的ThreadLocalMap,所以就不存在多线程竞争资源问题,所以如果使用ThreadLocal建议和线程一起使用,以为这样可以减少系统的性能开销以及对ThreadLocal对象的一种复用,提升系统性能。

看一下ThreadLocalMap,会发现Entry继承了WeakReference类,说明这个类创建出来的对象是弱引用对象

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;
//构造初始化table数组
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);
}

我们开看一下get方法:获取当前线程,然后通过调用getMap方法获取到当前线程的ThreadLocalMap对象,对于刚开始初始化的线程或者在ThreadLocalMap中没有找到来说,那么就会走下面的setInitialValue方法,如果已经初始化的ThreadLocalMap来说,会直接获取对应的Entry对象

    /**
     * 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);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

在setInitialValue方法中,会发现去获取初始化方法中的对象,如果在创建ThreadLocal没有重写初始化方法的话,那么就会返回null,或者在使用前调用set方法重新设置一下当前线程中的ThreadLocalMap中的属性初始化值,大家可以各自去看一下set方法;

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

这里列一下getEntry方法,根据ThreadLocal对象计算出hash值找到对应的table数组的位置,并且获取到这个对象。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

问题

以为这就完了?并没有,规范的使用为在使用过get方法应该在调用remove方法进行删除(即将当前线程ThreadLocalMap中的引用置为空并且table数组中的Entry值也置为空);如果是线程池的话,那么这些数据就会一直存在,如果没有及时删除会造成内存泄漏。

调用remove方法回去执行ThreadLocalMap的remove方法,而在这个方法中,通过计算得到对应的Entry数组的位置,并且进行引用清除以及table数组清空,避免内存泄漏问题,

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
/**
 * Remove the entry for key.
 */
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) {
            //将Entry本身的引用ThreadLocal置为空
            e.clear();
            //清空table数组中Entry.value
            expungeStaleEntry(i);
            return;
        }
    }
}

但是如果是经常性用到的ThreadLocal的话,个人建议可以不用删除,因为如果频繁使用的话,置为null,后面又会重新调用在ThreadLocalMap未找到,那么就会调用setInitialValue方法,重新创建对象并且赋值,在某种意义上可以说是和局部变量是一样的了,这样就违背了当初减少性能开销的需求了。

在加上Entry继承了WeakReference类,所以创建的对象会是个弱引用类型,在GC进行回收时候会被回收掉的,如果回收掉了引用对象,那么Entry中的value变量值是否还存在呢;

继续解析

注意这里是回收掉引用的对应,即ref.get()为null值,但是弱引用本身这个对象是还在的,我们看一下在setInitialValue方法中是如何处理的,重新设置里面,如果获取到ThreadLocal引用没有获取到,说明这个弱引用被回收了,这里就会去调用replaceStaleEntry方法。

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

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

    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();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

 而在replaceStaleEntry方法中有个这么一行代码,将value变量置为null并且重新创建Entry对象,所以就算是没有调用remove删除方法,在GC过后依旧会置为null。

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

在此会有个小问题,为啥不用SoftReference而是使用WeakReference,个人觉得如果使用软引用的话,如果是使用线程池并且ThreadLocal会频繁访问的话,那么是可以的,但是实际应用并非只有这种情况,而且在发生OOM之前,只会回收掉软引用对象,但是Entry中的value变量还在,并不能真正的回收掉值,只有等到下一次使用的时候才能置为null,所以综合来看使用WeakReference还是最好的选择。

欢迎各位大佬一起讨论

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值