ThreadLocal详解

ThreadLocal很容易让人望文生义,认为是一个本地线程,其实不然,ThreadLocal是Thread的一个局部变量,TheradLocal本身是一个类,是用于解决多线程并发访问问题。它为每一个线程提供了变量副本,使得每个线程在同一时刻访问到的并非同一个对象,从而隔离了多个线程对数据的共享。

1.不使用ThreadLocal,变量共享

 

 

  这个例子很简单,就是开启5个线程,对一个变量进行++操作,当这个变量是一个普通Integer类型时,线程共享这个变量,运行结果如下:

2.使用ThreadLocal,变量隔离,每个线程有自己专属的本地变量

而当我们使用ThreadLocal时,是为每个线程提供了一个变量的副本

 

 

 其运行结果如下:

3.ThreadLocal类结构

我们先来看下ThreadLocal这个类,部分源码如下:

ThreadLocal有一个内部内ThreadLocalMap,ThreadLocalMap中有一个Entry类型的数组,而这个Entry是一个key,value类型的,key是ThreadLocal本身,value就是Threadlocal携带的值。每个Thread都持有一个ThreadlLocalMap,通过ThreadLocal来维护这个ThreadLocalMap,向map中设置和获取变量副本。

4.源码分析

4.1initialValue()方法

返回当前线程局部变量的初始值,是一个protected修饰的方法,可以用来重写,上述例子用重写该方法设置初始值为0.

    protected T initialValue() {
        return null;
    }

4.2get()方法

  public T get() {
     //获取当前线程 Thread t
= Thread.currentThread();
     //获取当前线程所持有的ThreadLocalMap ThreadLocalMap map
= getMap(t);
     //不为空
if (map != null) {
       //获取以当前ThreadLocal为key的Entry ThreadLocalMap.Entry e
= map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked")
          //获取Entry中的value值并返回 T result
= (T)e.value; return result; } }
     //设置初始值
return setInitialValue(); }

getMap(t)方法,获取当前线程所持有的ThreadLocalMap

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

 

getEntry(this)方法,获取以当前ThreadLocal为key的Entry,然后通过Entry.value获取变量副本

  private Entry getEntry(ThreadLocal<?> key) {
    //计算以当前ThreadLocal为key的Entry在数组中的位置
int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i];
    //数组中该位置的Entry不为空且key相等则返回此Entry
if (e != null && e.get() == key) return e; else
       //为空则返回null return getEntryAfterMiss(key, i, e);   }

setInitiaValue()方法,获取当前线程所持有的ThreadLocalMap,map为空就创建,不为空就设置初始值,然后返回初始值,如果没重写initialValue()方法,则初始值为null,否则就是重写的方法中返回的值

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

get方法就是获取当前线程,根据当前线程获取其所持有的ThreadLocalMap,然后以当前ThreadLocal为key从ThreadlocalMap获取Entry,再从Entry获取值返回。

4.3set()方法,获取当前线程 持有的ThreadLocalMap,往map中存储值

    public void set(T value) {
        Thread t = Thread.currentThread();
     //获取当前线程所持的ThreadLocalMap,把value设置到到以当前ThreadLocal为Key的Entry中 ThreadLocalMap map
= getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }

调用ThreadLocalMap的set()方法,根据key计算位置,把值存储在对应位置的Entry中

      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; }           //key为null,但是存在Entry,表示原本的ThreadLocal已经被回收,这是一个旧的Entry,使用新的Entry进行替换 if (k == null) { replaceStaleEntry(key, value, i); return; } }        //key不存在且对应位置没有Entry,那么就创建一个新的Entry tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

set方法就是获取当前线程,根据当前线程获取其所持有的ThreadLocalMap,如果map为空就创建,不为空,则通过当前ThreadLocal为key计算位置,把值放入对应位置的Entry中。

5.使用场景

1.数据传递:通过ThreadLocal在同一个线程,不同的组件的传递数据,避免传参带来的耦合。

2.线程隔离:使得每个线程都有一个变量的副本,在并发中使用这个变量也相互独立,互不影响。

spring的事务就是使用的ThreadLocal,spring从数据库连接池中获取一个Connection,然后把Connection放入ThreadLocal中与线程绑定,来保证service层和dao层使用的是用一个connection。

6.内存泄漏

Threadlocal虽然好用,然后如果用的不好,可能会导致内存泄漏。我们在回归到ThreadLocalMap的结构

可以看到ThreadLocalMap中的Entry使用ThreadLocal的弱引用作为key,弱引用:弱引用的对象发生gc时就会被回收。例

以上述代码为例,创建一个线程池,线程数量为5,使用线程池执行100个任务,任务所做的事就是使用ThreadLocal携带一个LocalVariable对象,这个对象就是创建一个5M大小的数组。对上述代码进行内存分析。

从代码可以得到上图,以一个线程为例,栈中有一个Threadlocal的引用,一个当前线程的引用,而这个线程持有一个ThreadLcalMap对象,map中又有一个Entry的数组,数组中有一个Entry,这个Entry的key是ThreadLocal的弱引用,value是一个LocalVaribale对象,对象中含有一个5M大小的数组。栈内存是线程私有的,堆内存是线程共享的,方法的调用就是一个入栈、出栈的过程(这个涉及JVM的知识),随着方法的调用完成,栈中的东西就可以释放,所以当我们的run()方法运行完,栈中指向堆内存中ThreadLocal对象的引用可能就断掉了,那么这个时候堆内存中的ThreadLocal对象就没有了强引用,只有一个ThreadLocal的弱引用,在下一次gc的时候ThreaLocal对象就会被回收,那么这个时候Entry中key就为null了,而我们的当前线程仍在运行,执行下一个任务,所以Entry的强引就一直存在,那么这个Entry对象、value不会被回收,而这个value是通过Entry来获取,而Entry又需要通过这个key来获取,而这个时候的key为了null,所以这个value值就永远都获取不到,于是就可能造成了内存泄漏。

通过分析可以知道内存泄漏的是原因是因为key为nul,导致vlaue无法被访问,同时也没有回收,而key为null的原因是因为它是一个弱引用。但其实弱引用不是导致内存泄漏的原因,这个弱引用其实是用来避免内存泄漏的,为什么这么说呢?如果这个key是一个强引用,value是可以被访问到,但是ThreadLocal对象则一直存在一个强引用,线程不结束,那么这个对象就永远不会被回收,那么一定会内存泄漏,而使用弱引用的话,这个ThreadLocal对象肯定会被回收,而value其实是可能会被回收的,大腿们早就考虑这个情况,所以在set、get方法中在某些情况下会调用expungeStaleEntry()方法,这个方法就是会把key为null的Entry置为空,而remove方法则一定会调用expungeStaleEntry()方法,所以避免内存泄漏的方法就是在使用完ThredLocal后,调用remove()方方法。所以导致内存泄漏的根本原因ThreadLocalMap的生命周期和Thread一样长,如果不手动调用remove方法,删除key为null的Entry,那么就可能会导致内存泄漏,而不是因为弱引用。

posted @ 2021-08-01 21:48  福福猿  阅读(176)  评论(0)    收藏  举报