ThreadLocal 个人见解

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

即:在线程中保存一个局部变量,在该线程执行过程,获取时一定能获取到上次设置的值(前提不进行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()方法进行清除

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容