ThreadLocal详解(1)

一、概念

ThreadLocal用来提供线程内部的局部变量,该变量在多线程情况下,可以提供线程隔离,使得局部变量仅在线程内部被访问到。

ThreadLocal的基本API:

  • set(),设置当前线程绑定到局部变量
  • get(),获取当前线程绑定的局部变量
  • remove(),移除当前线程绑定的局部变量

二、内部原理

在JDK8以后,每个Thread内部都持有一个ThreadLocalMap,该Map的key为ThreadLocal本身,value为我们要与该线程绑定的局部变量。所以,当一个线程获取与自己绑定的局部变量时,只需要将ThreadLocal作为key传递给ThradLocalMap即可查到。我们申请一个ThreadLocal对象,只要其与不同的线程绑定,就可以达成针对不同线程,访问同一个ThreadLocal对象,但是数据是每个线程特有的。因为ThreadLocal只是作为key,每个线程自己持有的ThreadLocalMap是不同的,key相同,但value可能不一样。

在这里插入图片描述

三、核心方法源码

  • set()方法:
public void set(T value){
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t);//获取当前线程持有的ThreadLocalMap
    if(map != null){
      //当前线程的map不为空,调用ThreadLocalMap的set方法,
      //将自己(ThreadLocal)作为Key,value作为值存入Map中
      map.set(this,value);
    }else{
      //如果当前线程没有ThreadLocalMap对象,则调用creatMap方法进行创建
      creatMap(t,value);
    }
}

ThreadLocalMap getMap(Thread t){
   return t.thradLocals;
}
//当前线程的map为null时,为当前线程创建map,并初始化,将该ThreadLocal和value
//组成一个Entry存入。当set方法创建map时,value为我们传入set方法的value,
//当使用get方法创建时,value为initialValue()方法的返回值,默认为null
void createMap(Thread t, T firstValue){
  t.threadLocals = new ThreadLocalMap(this,firstValue);
}
  • get()方法:
public T get(){
 
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t);
    if(map != null){
        //取值时,将ThreadLocal作为key传入,去map中查询有无对应的value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if(e != null){
          //查询到的节点(即Entry,因为map实际上是一个Entry数组,
          //每个Entry包含一个Key和一个Value)
          @SuppressWarnings("unchecked")
          T result = (T)e.value;
          return result;
        }
    }
    //当map不存在或者map存在但是要没有当前ThreadLocal作为key所关联的Entry时
    //调用该方法为当前线程创建map或者为该ThreadLocal创建一个Entry,
    //并将value设置为null(默认为null),最后将Entry插入到map中
    return setInitialValue();
}

private T setInitialValue(){
    //默认返回为null,用户可以重写该方法,相当于为一个没有与我们想要的局部变量绑定的ThreadLocal设置初值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(map != null){
      map.set(this,value);
    }else{
      //如果当前线程没有ThreadLocalMap对象,则调用creatMap方法进行创建
      creatMap(t,value);
    }
    return value;//返回我们为该ThreadLocal设置的默认值
}
//为当前ThreadLocal的分配初值,以便组成Entry
protected T initialValue(){
  return null;
}
  • remove()方法:
public void remove(){
  ThreadLocalMap map = getMap(Thread.currentThread());
  if(map != null){
     //当threadLocalMap对象不为空时,将该ThreadLocal作为key的Entry从数组中删除
     map.remove(this);
  }
} 

四、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,而是独立实现了Map的功能,内部的Entry也是独立实现的。

在这里插入图片描述
根据类图,我们可以得知,Entry的Key继承了弱引用类。所以,在第二部分的图中,ThreadLocalMap中的Entry是通过弱引用指向ThreadLocal的,而用户通过一个强引用指向ThreadLocal,然后就可以将该ThreadLocal传入到线程的map中来获取值或者存值。

ThreadLocalMap的初始容量默认为16,同时持有一个int类型的size变量(默认为0),用来记录当前ThreadLocalMap中含有的元素(Entry)个数,同时还有一个int类型的threshold用来记录进行扩容的阈值(当数组中元素的个数大于该值,数组进行扩容,默认为0)。

成员变量

/**
* 初始容量 - 必须是2的整次幂
**/
private static final int INITIAL_CAPACITY = 16;

/**
*存放数据的table ,Entry类的定义在下面分析,同样,数组的长度必须是2的整次幂
**/
private Entry[] table;

/**
*数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值
**/
private int size = 0;

/**
*进行扩容的阈值,表使用量大于它的时候进行扩容
**/
private int threshold; // Default to 0
  • set()方法:
private void set(ThreadLocal<?> key,Object value){
   //取得当前线程维护的ThreadLocalMap中的Entry数组。
   ThreadLocal.ThreadLocalMap.Entry[] tab = table;
   int len = tab.length;
   //计算索引
   int i = key.threadLocalHashCode & (len-1);
   //使用线性探测法解决hash冲突
   for(ThreadLocal.ThreadLocalMap.Entry e = tab[i];e!=null;e=tab[i = nexIndex(i,len)]){
     ThreadLocal<?> k = e.get();
     //如果ThreadLocal对应的Entry以前有值,直接覆盖
     if(k == key){
        e.value = value;
        return;
     }
     //如果key为null,即原来该ThreadLocal所对应的Entry没有被回收,
     //但是ThreadLocal被回收了
     if(k == null){
       //此时,用新元素替代旧元素
       replaceStaleEntry(key,value,i);
       return;
     }
   }
   //如果数组中不存在ThreadLocal所对应的Entry,则直接插入即可
   tab[i] = new Entry(key,value);
   int sz = ++size;
   /*如果没有key为null的元素而且Entry数组的大小大于等于了阈值,进行扩容
   */
   if(!cleanSomeSlots(i,sz) && sz >= threshold)
      rehash();
}

private static int nextIndex(int i, int len){
   return ((i+1 < len) ? i+1 : 0);
}
/*
将key为null的Entry的value也设置为null
*/
private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

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

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

在这里,index的计算使用了hashCode & (len - 1).此处这样的处理是和HashMap中index的计算原理相同的。

五、Entry定义

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

            Entry(ThreadLocal<?> k, Object v) {
                super(k);//设置key的引用类型为弱引用
                value = v;
            }
}

为什么要将ThreadLocalMap中Entry持有的key的引用类型设置为弱引用呢?
这是因为如果我们使用强引用的话,当我们用户使用完ThreadLocal对象后,我们用户持有的指向ThreadLocal的引用就被回收掉了,此时逻辑上来说应该把堆上ThreadLocal对象也给回收掉,但是此时ThreadLocalMap中还有一个Entry的Key持有一个指向ThreadLocal的强引用,导致ThreadLocal无法被JVM回收。造成内存泄漏。而使用弱引用的话,当JVM垃圾回收时,弱引用是不管内存是否充足,都会被直接回收的。所以,当我们用户持有的强引用被回收后,则下次垃圾回收的时候,就会把我们创建的ThreadLocal对象回收掉(此时,只有一个Entry的弱引用指向它),但是此时还是会存在内存泄漏问题。

这是因为,虽然ThreadLocal对象被回收了,但是此时Entry对象还没有被回收,还是被线程中的ThreadLocalMap存着一个指向它的强引用,此时Entry的状态是key为null,value指向我们存入的value,因此这个Entry永远不会被访问到了,但它也没法被回收,所以还是存在内存泄漏问题。使用弱引用只是解决了ThreadLocal造成的内存泄漏问题,但依然存在着Entry的内存泄漏问题。

如何解决呢?每次使用完一个线程局部变量,就是用remove()方法将其从ThreadLocalMap中移除。(当然如果线程运行完毕,则也不会产生内存泄漏,因为ThreadLocalMap也被回收掉了)

除此之外,如果下次我们还会使用ThreadLocalMap的get或者set方法,此时ThreadLocalMap会对key为null的Entry进行清除(前面第四部分的源码可以得知这点)。所以,使用弱引用会保证当一个ThreadLocal不会再被使用后,ThreadLocal一定会被回收,其Entry会在下次调用ThreadLocalMap的get、set、remove方法时清除,避免内存泄漏。(但是,当我们一个线程只是用一次ThreadLocal,后面线程一直运行,但不会再次使用ThreadLocal的情况会造成内存泄漏,所以最好还是用完就进行remove)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值