美文网首页
ThreadLocal 个人见解

ThreadLocal 个人见解

作者: 二哥很猛 | 来源:发表于2020-04-17 13:23 被阅读0次

    线程本地变量保存,与并发实际上并没关系

    即:在线程中保存一个局部变量,在该线程执行过程,获取时一定能获取到上次设置的值(前提不进行remove或者设置为null)

    get过程

    //获取保存的变量
       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;
                }
            }
         //线程中还没创建保存变量的Map或者没有找到,则直接初始化默认变量值并创建Map
            return setInitialValue();
        }
    
        //获取value
      private Entry getEntry(ThreadLocal<?> key) {
        //通过Hash获取指定的下标
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
          return e;
        else
          //没找到(hash冲突了)
          return getEntryAfterMiss(key, i, e);
      }
    
    //通过开放定址法进行查找(存在hash时,查找该下标后一位的值进行判断)
      private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    
        while (e != null) {
          ThreadLocal<?> k = e.get();
          if (k == key)
            return e;
          if (k == null)
            //发现有Entry不为空,key为空的节点,可能key已经被垃圾回收了,值还没被回收,因此需要清除
            expungeStaleEntry(i);
          else
            //当前下标的下一位
            i = nextIndex(i, len);
          e = tab[i];
        }
        return null;
      }
    
    //清除staleSlot及其节点后key=null的Entry,同时返回最近一个Entry为空的节点的下标
      private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
    
        //先把当前给清了
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;  
        Entry e;
        int i;
        //依次向后查找,如果发现key还有为空的,依旧清除
        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);
            //按正常来说,k在该位置但通过hash判断不是在该位置,说明k元素也是存在hash冲突被移过来的
            //将其归为到原来应该待的地方(因为这个位置可能已经腾出来了),如果已经存在冲突,则依次向后查找一个空位置放进去
            if (h != i) {
              tab[i] = null;
              while (tab[h] != null)
                h = nextIndex(h, len);
              tab[h] = e;
            }
          }
        }
        return i;
      }
    

    set过程

      private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
            //先进行hash冲突判断
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
          ThreadLocal<?> k = e.get();
          if (k == key) {
            e.value = value;
            return;
          }
                //该位置虽然被人占坑,但是key已经过期,可以替换为新的key和value
          if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
          }
        }
            //没有hash冲突或一直存在hash冲突(即上述过程没有成功)
        tab[i] = new Entry(key, value);
        int sz = ++size;
        //新增或删除时,会重新清理过期的数据
        //该处表示没有清除掉过期的数据(所有数据都有用),则会进行扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
      }
    
    
    //替换元素操作,同时尽可能的清理掉已经"过期"的数据
      private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                             int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
        //应该放在staleSlot位置,但是先向前操作一波,看看是否还有key=null的Entry节点
        int slotToExpunge = staleSlot;
        for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) {
          if (e.get() == null) {
             slotToExpunge = i;
          }
        }
      
        // 向后查找,虽然要放在staleSlot位置,但是前面也仅仅判断该位置为null(这个位置可能是其他元素先占的,只是后面被清除了而已)
        //因此需要向后查找,看看是否已经真的存在当前key的元素
        for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
          ThreadLocal<?> k = e.get();
          //发现后面确实存在
          if (k == key) {
            e.value = value;
            //调换位置 注意tab[staleSlot]是个key为null的不为空的Entry
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
                    //此处表示在staleSlot节点前并没发现key=null的Entry节点存在,且staleSlot~i之间也没有发现key=null的Entry节点
            if (slotToExpunge == staleSlot) {
              //目前也只是遍历(0-slotToExpunge[都不为空])
              //甚至staleSlot-i之间都可能存在key=null的Entry存在,
              //同样i-len之间依旧可能存在key=null的Entry存在
               slotToExpunge = i;
            }
            //expungeStaleEntry清除从i到len之间key=null的Entry节点的数据
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
          }
          //当前节点为空,且(0-slotToExpunge)全部有值,记录第一次出现key=null的Entry节点的位置,方便清除
          if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
        }
        //自始至终没找到后面与之相同key
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);
    //但是在遍历的过程中找到了key=null的Entry节点,清除操作
        if (slotToExpunge != staleSlot)
          cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
      }
    
    //尝试遍历从i-n之间的Entry节点,发现"过期"的entry则删除
    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];
          //发现有过期的元素,则直接从len/2处向后遍历
          if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
          }
          //此处个人感觉减少无用的遍历
        } while ( (n >>>= 1) != 0);
        return removed;
      }
    

    remove过程

    
    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) {
            e.clear();
            //清除自身的同时也会将i之后的某些key=null的Entry清除了
            expungeStaleEntry(i);
            return;
          }
        }
      }
    
    

    expungeStaleEntry与cleanSomeSlots总结

    • expungeStaleEntry 遍历 从指定下标x到最近一个Entry(下标为y) 即 (x-y)之间所有Entry是否存在key=null的元素,如果存在则清除,注意 y的下标不是人为可控的(只要发现有Entry为空则暂停后续的清除操作)
    • cleanSomeSlots 清除(x-n)节点所有key=null的Entry节点的值,n的位置是可控的

    内存溢出问题(个人总结)

    由于ThreadLocalMap中Entry的key是弱引用,而key=ThreadLocal,在一般情况下 我们定义一个ThreadLocal都是staitc final的(官方也这么建议),因此key=null的可能性几乎为零,弱引用本身的作用无法提现出来,key和value都是强引用 (还请大佬能指明一下)

    内存溢出主要是在线程池中,我们一般使用Tomcat最为web容器,而Tomcat接收请求后交给线程池来处理我们的业务请求,因此线程无法被销毁,线程变量永远不会清空会造成内存泄露,尤其是项目中大规模使用ThreadLocal或者存储过多数据时

    尽管set get 操作会在一定情况下清除key=null value不为空的数据,但是由于我们经常是用final,很少存在key=null的情况

    因此强制建议在使用完线程变量后调用remove()方法进行清除

    相关文章

      网友评论

          本文标题:ThreadLocal 个人见解

          本文链接:https://www.haomeiwen.com/subject/npbevhtx.html