ThreadLocal 個(gè)人見解

線程本地變量保存,與并發(fā)實(shí)際上并沒關(guān)系

即:在線程中保存一個(gè)局部變量,在該線程執(zhí)行過程,獲取時(shí)一定能獲取到上次設(shè)置的值(前提不進(jìn)行remove或者設(shè)置為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;
            }
        }
     //線程中還沒創(chuàng)建保存變量的Map或者沒有找到,則直接初始化默認(rèn)變量值并創(chuàng)建Map
        return setInitialValue();
    }

    //獲取value
  private Entry getEntry(ThreadLocal<?> key) {
    //通過Hash獲取指定的下標(biāo)
    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);
  }

//通過開放定址法進(jìn)行查找(存在hash時(shí),查找該下標(biāo)后一位的值進(jìn)行判斷)
  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)
        //發(fā)現(xiàn)有Entry不為空,key為空的節(jié)點(diǎn),可能key已經(jīng)被垃圾回收了,值還沒被回收,因此需要清除
        expungeStaleEntry(i);
      else
        //當(dāng)前下標(biāo)的下一位
        i = nextIndex(i, len);
      e = tab[i];
    }
    return null;
  }

//清除staleSlot及其節(jié)點(diǎn)后key=null的Entry,同時(shí)返回最近一個(gè)Entry為空的節(jié)點(diǎn)的下標(biāo)
  private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    //先把當(dāng)前給清了
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;  
    Entry e;
    int i;
    //依次向后查找,如果發(fā)現(xiàn)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沖突被移過來的
        //將其歸為到原來應(yīng)該待的地方(因?yàn)檫@個(gè)位置可能已經(jīng)騰出來了),如果已經(jīng)存在沖突,則依次向后查找一個(gè)空位置放進(jìn)去
        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);
        //先進(jìn)行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已經(jīng)過期,可以替換為新的key和value
      if (k == null) {
        replaceStaleEntry(key, value, i);
        return;
      }
    }
        //沒有hash沖突或一直存在hash沖突(即上述過程沒有成功)
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //新增或刪除時(shí),會重新清理過期的數(shù)據(jù)
    //該處表示沒有清除掉過期的數(shù)據(jù)(所有數(shù)據(jù)都有用),則會進(jìn)行擴(kuò)容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
  }


//替換元素操作,同時(shí)盡可能的清理掉已經(jīng)"過期"的數(shù)據(jù)
  private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                         int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    //應(yīng)該放在staleSlot位置,但是先向前操作一波,看看是否還有key=null的Entry節(jié)點(diǎn)
    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(這個(gè)位置可能是其他元素先占的,只是后面被清除了而已)
    //因此需要向后查找,看看是否已經(jīng)真的存在當(dāng)前key的元素
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
      ThreadLocal<?> k = e.get();
      //發(fā)現(xiàn)后面確實(shí)存在
      if (k == key) {
        e.value = value;
        //調(diào)換位置 注意tab[staleSlot]是個(gè)key為null的不為空的Entry
        tab[i] = tab[staleSlot];
        tab[staleSlot] = e;
                //此處表示在staleSlot節(jié)點(diǎn)前并沒發(fā)現(xiàn)key=null的Entry節(jié)點(diǎn)存在,且staleSlot~i之間也沒有發(fā)現(xiàn)key=null的Entry節(jié)點(diǎn)
        if (slotToExpunge == staleSlot) {
          //目前也只是遍歷(0-slotToExpunge[都不為空])
          //甚至staleSlot-i之間都可能存在key=null的Entry存在,
          //同樣i-len之間依舊可能存在key=null的Entry存在
           slotToExpunge = i;
        }
        //expungeStaleEntry清除從i到len之間key=null的Entry節(jié)點(diǎn)的數(shù)據(jù)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        return;
      }
      //當(dāng)前節(jié)點(diǎn)為空,且(0-slotToExpunge)全部有值,記錄第一次出現(xiàn)key=null的Entry節(jié)點(diǎn)的位置,方便清除
      if (k == null && slotToExpunge == staleSlot)
        slotToExpunge = i;
    }
    //自始至終沒找到后面與之相同key
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
//但是在遍歷的過程中找到了key=null的Entry節(jié)點(diǎn),清除操作
    if (slotToExpunge != staleSlot)
      cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
  }

//嘗試遍歷從i-n之間的Entry節(jié)點(diǎn),發(fā)現(xiàn)"過期"的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];
      //發(fā)現(xiàn)有過期的元素,則直接從len/2處向后遍歷
      if (e != null && e.get() == null) {
        n = len;
        removed = true;
        i = expungeStaleEntry(i);
      }
      //此處個(gè)人感覺減少無用的遍歷
    } 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();
        //清除自身的同時(shí)也會將i之后的某些key=null的Entry清除了
        expungeStaleEntry(i);
        return;
      }
    }
  }

expungeStaleEntry與cleanSomeSlots總結(jié)

  • expungeStaleEntry 遍歷 從指定下標(biāo)x到最近一個(gè)Entry(下標(biāo)為y) 即 (x-y)之間所有Entry是否存在key=null的元素,如果存在則清除,注意 y的下標(biāo)不是人為可控的(只要發(fā)現(xiàn)有Entry為空則暫停后續(xù)的清除操作)
  • cleanSomeSlots 清除(x-n)節(jié)點(diǎn)所有key=null的Entry節(jié)點(diǎn)的值,n的位置是可控的

內(nèi)存溢出問題(個(gè)人總結(jié))

由于ThreadLocalMap中Entry的key是弱引用,而key=ThreadLocal,在一般情況下 我們定義一個(gè)ThreadLocal都是staitc final的(官方也這么建議),因此key=null的可能性幾乎為零,弱引用本身的作用無法提現(xiàn)出來,key和value都是強(qiáng)引用 (還請大佬能指明一下)

內(nèi)存溢出主要是在線程池中,我們一般使用Tomcat最為web容器,而Tomcat接收請求后交給線程池來處理我們的業(yè)務(wù)請求,因此線程無法被銷毀,線程變量永遠(yuǎn)不會清空會造成內(nèi)存泄露,尤其是項(xiàng)目中大規(guī)模使用ThreadLocal或者存儲過多數(shù)據(jù)時(shí)

盡管set get 操作會在一定情況下清除key=null value不為空的數(shù)據(jù),但是由于我們經(jīng)常是用final,很少存在key=null的情況

因此強(qiáng)制建議在使用完線程變量后調(diào)用remove()方法進(jìn)行清除

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末莲趣,一起剝皮案震驚了整個(gè)濱河市毅待,隨后出現(xiàn)的幾起案子拂到,更是在濱河造成了極大的恐慌,老刑警劉巖法挨,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谁榜,死亡現(xiàn)場離奇詭異,居然都是意外死亡凡纳,警方通過查閱死者的電腦和手機(jī)窃植,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惫企,“玉大人撕瞧,你說我怎么就攤上這事陵叽。” “怎么了丛版?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵巩掺,是天一觀的道長。 經(jīng)常有香客問我页畦,道長胖替,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任豫缨,我火速辦了婚禮独令,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘好芭。我一直安慰自己燃箭,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布舍败。 她就那樣靜靜地躺著招狸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邻薯。 梳的紋絲不亂的頭發(fā)上裙戏,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機(jī)與錄音厕诡,去河邊找鬼累榜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛灵嫌,可吹牛的內(nèi)容都是我干的壹罚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼寿羞,長吁一口氣:“原來是場噩夢啊……” “哼渔嚷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起稠曼,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎客年,沒想到半個(gè)月后霞幅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡量瓜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年司恳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绍傲。...
    茶點(diǎn)故事閱讀 40,498評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扔傅,死狀恐怖耍共,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情猎塞,我是刑警寧澤试读,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站荠耽,受9級特大地震影響钩骇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铝量,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一倘屹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧慢叨,春花似錦纽匙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至赠尾,卻和暖如春力穗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背气嫁。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工当窗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寸宵。 一個(gè)月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓崖面,卻偏偏與公主長得像,于是被迫代替她去往敵國和親梯影。 傳聞我的和親對象是個(gè)殘疾皇子巫员,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容