ThreadLocal详解

ThreadLocal可能大家都有耳闻,线程局部变量-同样的ThreadLocal对象在不同线程中获取到的值不同,用于并发场景确保线程安全使用。

那么它是如何实现线程局部变量并确保线程安全的呢,我们一起来看下ThreadLocal的底层实现。

底层实现

话不多说,直接上代码,一起来看看ThreadLocal的核心代码

get方法


//通过ThreadLocal获取我们需要的线程局部变量的方法

public T get() {

    Thread t = Thread.currentThread();

    ThreadLocal.ThreadLocalMap map =this.getMap(t);

    if (map !=null) {

           ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);

           if (e !=null) {

                  T result = e.value;

                  return result;

        }

}

     return this.setInitialValue();

}

从代码来看,ThreadLocal的get方法中尝试从当前线程Thread对象中的ThreadLocalMap中,取出以自身为key的value并返回,如果尝试失败,即不存在对应的value值,则调用setInitialValue方法并返回其返回值,具体流程图如下:

image

因此我们可以看出,真正的线程局部变量是存储在Thread对象中的,ThreadLocal只是一个key,在不同的线程中,用同一个key能取出不同的value,是因为不同线程中的map是线程独有的。

而setInitialValue方法,看命名是初始化value的方法,不过这个方法的调用时机却是在get中取不到value时调用,可以看成是get方法的兜底策略,也可以看成是这个初始化方法是个懒加载,需要的之后才执行,接下来我们来看看这个兜底方法。

setinitialValue方法


private T setInitialValue() {

    T value =this.initialValue();

    Thread t = Thread.currentThread();

    ThreadLocal.ThreadLocalMap map =this.getMap(t);

    if (map !=null) {

          map.set(this, value);

    } else {

          this.createMap(t, value);

    }

    if (this instanceof TerminatingThreadLocal) {

          TerminatingThreadLocal.register((TerminatingThreadLocal)this);

    }

    return value;

}

可以看到setInitialValue方法中,调用了initialValue方法,获取到初始的value,然后以自身为key,将初始的value写入当前线程的ThreadLocalMap中,所以initialValue方法才是初始值的真正来源,使用方可以根据需要自定义initialValue方法。

除此之外,ThreadLocal中还有两个较为重要的方法,set和remove方法

set方法


public void set(T value) {

    Thread t = Thread.currentThread();

    ThreadLocal.ThreadLocalMap map =this.getMap(t);

    if (map !=null) {

          map.set(this, value);

    } else {

          this.createMap(t, value);

    }

}

通过该方法可指定value并更新写入到当前线程的ThreadLocalMap中

remove方法


public void remove() {

    ThreadLocal.ThreadLocalMap m =this.getMap(Thread.currentThread());

    if (m !=null) {

           m.remove(this);

    }

}

该方法比较简单,就是从当前线程中的ThreadLocalMap中将ThreadLocal自身为key的键值对清理掉。

但是该方法的应用算是一个ThreadLocal的小进阶,会涉及到内存泄漏的问题,这一点我们在后面会讲到。

模型结构

通过对ThreadLocal源码的分析,我们已经知道了真正的线程局部变量是存储在每个线程的Thread对象中的ThreadLocalMap中,而这个map是以Thread为key,所以,同一个ThreadLocal在不同的线程中能拿到不同的值。整体模型如下:

image

ThreadLocal对象存在于堆中,线程共享,而在不同的线程Thread对象中的ThreadLocalMap,相同的ThreadLocal对应不同的value,而value才是我们所说的线程局部变量。

ThreadLocal的使用


public class ThreadMesHolder {

      //设置线程局部变量初始值

      private static ThreadLocalthreadMes =new ThreadLocal<>(){

                     @Override

                      protected StringinitialValue(){

                              return "hello world";

                      }

        };

    //获取线程局部变量

     public static StringgetMes(){

                 return threadMes.get();

        }

      //设置线程局部变量

     public static void setMes(String newMes){

                 threadMes.set(newMes);

      }

   //释放线程局部变量

    public static void clear(){

                  threadMes.remove();

     }

}

关于ThreadLocal的使用,这里写了一个简单的demo进行举例,demo中包含了对线程局部变量的获取、设置和清除操作,其中线程局部变量的清除经常会被忽视,部分场景下忘记清理使用后的局部变量可能会导致内存泄漏。我们来看看为什么会不及时调用ThreadLocal的remove方法会发生内存泄漏问题:


static class Entry extends WeakReference> {

    Object value;

    Entry(ThreadLocal k, Object v) {

        super(k);

        this.value = v;

    }

}

上面是ThreadLocalMap中entry的定义,可以看到在Entry中,key是用的弱引用类型,而弱引用的类型的回收时机是下一次垃圾回收,所以在线程使用完局部变量之后,如果没有及时调用remove方法对线程局部变量进行引用的释放,那么Entry中的key可能会被GC回收掉,而ThreadLocalMap中的value使用的是强引用不会被GC回收,那么此时Entry中只剩下value且对应key的引用被回收,无法通过key进行value引用的释放,在线程结束之前由于强引用的关系,该value一直不会被回收直到线程结束,从而导致内存泄漏。所以,在生命周期较长的线程中,对ThreadLocal的使用需要注意及时的调用remove方法释放局部变量的引用,避免内存泄漏问题。

//22.03.13更新
看代码的时候发现在ThreadLocal中有个方法expungeStaleEntry,该方法用于释放key为null的entry,调用场景为remove、replaceStaleEntry、cleanSomeSlots、getEntryAfterMiss、rehash这些方法被调用的时候,优化了内存泄漏问题

你可能感兴趣的:(ThreadLocal详解)