ThreadLocal詳解

介紹

  • 顧名思義這個類提供線程局部變量
  • 每個線程(通過其get或set方法)都有自己獨立初始化的變量副本

ThreadLocal思想

在多線程環(huán)境下秘遏,不同的線程同時訪問同一個共享變量會有并發(fā)問題说庭。一種解決方法是進行同步,例如使用synchronized。另外一種比較常見的形式就是局部(local)變量(這里排除局部變量引用指向共享對象的情況)野建,這樣資源就不是被兩個線程共享,那么也不會出現(xiàn)競爭問題垦搬。

自定義類實現(xiàn)ThreadLocal的功能

一個簡單的思路是使用 Map 存儲每個變量的副本贩绕,將當(dāng)前線程的 Name 作為 key,副本變量作為 value 值:

public class Test {

    /** 用于存儲每個線程對應(yīng)的數(shù)據(jù) */
    public static class CustomThreadLocal{
        public final Map<String,Integer> cacheValueMap=new HashMap<String, Integer>();
        private int defaultValue;

        public CustomThreadLocal(int value){
            defaultValue=value;
        }
        public void set(Integer value){
            cacheValueMap.put(Thread.currentThread().getName(),value);
        }
        public Integer get(){
            String threadName=Thread.currentThread().getName();
            if(cacheValueMap.containsKey(threadName)){
                return cacheValueMap.get(threadName);
            }
           return defaultValue;
        }
    }

    /** 數(shù)據(jù)資源類,提供進行加減操作*/
    public static class Number {
        private CustomThreadLocal value = new CustomThreadLocal(0);

        public void increase() throws InterruptedException {
            value.set(10);
            Thread.sleep(10);
            System.out.println("increase value: " + value.get());
        }
        public void decrease() throws InterruptedException {
            value.set(-10);
            Thread.sleep(10);
            System.out.println("decrease value: " + value.get());
        }
    }

    public static void main(String[] args) {

      final  Number number=new Number();

        Thread increaseThread=new Thread(new Runnable() {
            public void run() {
                try {
                    number.increase();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"th1");

        Thread decreaseThread=new Thread(new Runnable() {
            public void run() {
                try {
                    number.decrease();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"th2");

        increaseThread.start();
        decreaseThread.start();
    }
}

這種寫法存在的問題:

  • 即便線程執(zhí)行完赠法,只要 number 變量存在麦轰,線程的副本變量依然會存在(存放在 number 的 cacheMap 中)。
  • 多個線程有可能會同時操作 cacheMap砖织,需要對 cacheMap 進行同步處理

為了解決上面的問題款侵,我們換種思路,每個線程創(chuàng)建一個 Map侧纯,存放當(dāng)前線程中副本變量新锈,用 CustomThreadLocal 的實例作為 key 值,下面是一個示例:

public class Test {

   /** 自定義線程,并定義map存放當(dāng)前線程中副本變量 */
   public static class ManualThread extends Thread{
       public final Map<Integer,Integer> cacheValueMap=new HashMap<Integer, Integer>();
   }

   /** 通過實例本身映射出線程的副本數(shù)據(jù)眶熬,并對其進行操作 */
   public static class CustomThreadLocal{
       private int defaultValue;

       public CustomThreadLocal(int value){
           defaultValue=value;
       }
       public void set(Integer value){
           Integer id = this.hashCode();
           Map<Integer, Integer> cacheMap = getMap();
           cacheMap.put(id, value);
       }
       public Integer get(){
           Integer id = this.hashCode();
           Map<Integer, Integer> cacheMap = getMap();
           if (cacheMap.containsKey(id)) {
               return cacheMap.get(id);
           }
           return defaultValue;
       }
       //注意這個方法
       public Map<Integer, Integer> getMap() {
           ManualThread thread = (ManualThread) Thread.currentThread();
           return thread.cacheValueMap;
       }
   }

   /** 數(shù)據(jù)資源類,提供進行加減操作*/
   public static class Number {
       private CustomThreadLocal value = new CustomThreadLocal(0);

       public void increase() throws InterruptedException {
           value.set(10);
           Thread.sleep(10);
           System.out.println("increase value: " + value.get());
       }
       public void decrease() throws InterruptedException {
           value.set(-10);
           Thread.sleep(10);
           System.out.println("decrease value: " + value.get());
       }
   }

   public static void main(String[] args) {

     final  Number number=new Number();

     //使用自定義線程類
       Thread increaseThread=new ManualThread(){
           public void run() {
               try {
                   number.increase();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       };

       Thread decreaseThread=new ManualThread(){
           public void run() {
               try {
                   number.decrease();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       };

       increaseThread.start();
       decreaseThread.start();
   }
}

這種寫法妹笆,當(dāng)線程消亡之后块请,線程中存放的副本變量也會被全部回收,并且 cacheMap 是線程私有的拳缠,不會出現(xiàn)多個線程并發(fā)問題负乡。在 Java 中,ThreadLocal 類的實現(xiàn)就是采用的這種思想脊凰,注意只是思想,實際的實現(xiàn)和上面的并不一樣茂腥。

基本原理

ThreadLocal 的實現(xiàn)思想狸涌,我們在前面已經(jīng)說了,每個線程維護一個 ThreadLocalMap 的映射表最岗,映射表的 key 是 ThreadLocal 實例本身帕胆,value 是要存儲的副本變量。ThreadLocal 實例本身并不存儲值般渡,它只是提供一個在當(dāng)前線程中找到副本值的 key懒豹。 如下圖所示:


image.png

API總覽

get函數(shù)用來獲取與當(dāng)前線程關(guān)聯(lián)的ThreadLocal的值,如果當(dāng)前線程沒有該ThreadLocal的值驯用,則調(diào)用initialValue函數(shù)獲取初始值返回脸秽,initialValue是protected類型的,所以一般我們使用時需要繼承該函數(shù)蝴乔,給出初始值记餐。而set函數(shù)是用來設(shè)置當(dāng)前線程的該ThreadLocal的值,remove函數(shù)用來刪除ThreadLocal綁定的值薇正,在某些情況下需要手動調(diào)用片酝,防止內(nèi)存泄露

關(guān)鍵點分析

ThreadLocal 散列值

當(dāng)創(chuàng)建了一個 ThreadLocal 的實例后,它的散列值就已經(jīng)確定了挖腰,下面是 ThreadLocal 中的實現(xiàn)

private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

我們看到 threadLocalHashCode 是一個常量雕沿,它通過 nextHashCode() 函數(shù)產(chǎn)生。nextHashCode() 函數(shù)其實就是在一個 AtomicInteger 變量(初始值為0)的基礎(chǔ)上每次累加 0x61c88647猴仑,使用 AtomicInteger 為了保證每次的加法是原子操作审轮。而 0x61c88647 這個就比較神奇了,它可以使 hashcode 均勻的分布在大小為 2 的 N 次方的數(shù)組里宁脊。
具體散列測試請看http://www.reibang.com/p/fe9ffcf51f4b

ThreadLocalMap

被定義為一個靜態(tài)類断国,包含的主要成員:

  • 首先是Entry的定義;
  • 初始的容量為INITIAL_CAPACITY = 16榆苞;
  • 主要數(shù)據(jù)結(jié)構(gòu)就是一個Entry的數(shù)組table稳衬;
  • size用于記錄Map中實際存在的entry個數(shù);
  • threshold是擴容上限坐漏,當(dāng)size到達threashold時薄疚,需要resize整個Map碧信,threshold的初始值為len * 2 / 3;
  • nextIndex和prevIndex則是為了安全的移動索引

Entry

  • ThreadLocalMap 使用 Entry 類來存儲數(shù)據(jù)
  • entry的key是ThreadLocal實例街夭,value是Object(即我們所謂的“線程本地數(shù)據(jù)”)
  • 為避免占用空間較大或生命周期較長的數(shù)據(jù)常駐于內(nèi)存引發(fā)一系列問題砰碴,hash table的key是弱引用WeakReferences
static class Entry extends WeakReference <ThreadLocal <?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal <?> k, Object v) {
        super(k);
        value = v;
    }
}

set 函數(shù)

private void set(ThreadLocal <?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 根據(jù) ThreadLocal 的散列值,查找對應(yīng)元素在數(shù)組中的位置
    int i = key.threadLocalHashCode & (len - 1);
    // 使用線性探測法查找元素
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal <?> k = e.get();
        // ThreadLocal 對應(yīng)的 key 存在板丽,直接覆蓋之前的值
        if (k == key) {
            e.value = value;
            return;
        }
        // key為 null呈枉,但是值不為 null,說明之前的 ThreadLocal 對象已經(jīng)被回收了埃碱,當(dāng)前數(shù)組中的 Entry 是一個陳舊(stale)的元素
        if (k == null) {
            // 用新元素替換陳舊的元素猖辫,這個方法進行了不少的垃圾清理動作,防止內(nèi)存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // ThreadLocal 對應(yīng)的 key 不存在并且沒有找到陳舊的元素砚殿,則在空元素的位置創(chuàng)建一個新的 Entry啃憎。
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // cleanSomeSlot 清理陳舊的 Entry(key == null),具體的參考源碼似炎。如果沒有清理陳舊的 Entry 并且數(shù)組中的元素大于了閾值辛萍,則進行 rehash。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

set函數(shù)注意點

  • int i = key.threadLocalHashCode & (len - 1);羡藐,這里實際上是對 len-1 進行了取余操作贩毕。之所以能這樣取余是因為 len 的值比較特殊,是 2 的 n 次方仆嗦,減 1 之后低位變?yōu)槿?1耳幢,高位變?yōu)槿?0。例如 16欧啤,減 1 之后對應(yīng)的二進制為: 00001111睛藻,這樣其他數(shù)字中大于 16 的部分就會被 0 與掉,小于 16 的部分就會保留下來邢隧,就相當(dāng)于取余了店印。
  • 在 replaceStaleEntry 和 cleanSomeSlots 方法中都會清理一些陳舊的 Entry,防止內(nèi)存泄漏
    threshold 的值大小為 threshold = len * 2 / 3;
  • rehash 方法中首先會清理陳舊的 Entry倒慧,如果清理完之后元素數(shù)量仍然大于 threshold 的 3/4按摘,則進行擴容操作(數(shù)組大小變?yōu)樵瓉淼?2倍)
private void rehash() {
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

getEntry函數(shù)

private Entry getEntry(ThreadLocal <?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

因為 ThreadLocalMap 中采用開放定址法,所以當(dāng)前 key 的散列值和元素在數(shù)組中的索引并不一定完全對應(yīng)纫谅。所以在 get 的時候炫贤,首先會看 key 的散列值對應(yīng)的數(shù)組元素是否為要查找的元素,如果不是付秕,再調(diào)用getEntryAfterMiss方法查找后面的元素

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)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

所以首先e如果為null的話兰珍,那么getEntryAfterMiss還是直接返回null的,如果是不滿足e.get() == key询吴,那么進入while循環(huán)掠河,這里是不斷循環(huán)亮元,如果e一直不為空,那么就調(diào)用nextIndex唠摹,不斷遞增i爆捞,在此過程中一直會做兩個判斷:

  1. 如果k==key,那么代表找到了這個所需要的Entry,直接返回勾拉;
  2. 如果k==null煮甥,那么證明這個Entry中key已經(jīng)為null,那么這個Entry就是一個過期對象,這里調(diào)用expungeStaleEntry清理該Entry藕赞。 這里就解答了導(dǎo)致內(nèi)存泄露的原因苛秕,即ThreadLocal Ref銷毀時,ThreadLocal實例由于只有Entry中的一條弱引用指著找默,那么就會被GC掉,Entry的key沒了吼驶,value可能會內(nèi)存泄露的惩激,其實在每一個get,set操作時都會不斷清理掉這種key為null的Entry的蟹演。

為什么循環(huán)查找

主要是因為處理哈希沖突的方法风钻,我們都知道HashMap采用拉鏈法處理哈希沖突,即在一個位置已經(jīng)有元素了酒请,就采用鏈表把沖突的元素鏈接在該元素后面骡技,而ThreadLocal采用的是開放地址法,即有沖突后羞反,把要插入的元素放在要插入的位置后面為null的地方布朦,具體關(guān)于這兩種方法的區(qū)別可以參考:解決哈希(HASH)沖突的主要方法。所以上面的循環(huán)就是因為我們在第一次計算出來的i位置不一定存在key與我們想查找的key恰好相等的Entry昼窗,所以只能不斷在后面循環(huán)是趴,來查找是不是被插到后面了,直到找到為null的元素澄惊,因為若是插入也是到null為止的唆途。

分析完循環(huán)的原因,其實也可以深入expungeStaleEntry看看是怎么清理的掸驱。

expungeStaleEntry

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

看上面這段代碼主要有兩部分:

  1. expunge entry at staleSlot:這段主要是將i位置上的Entry的value設(shè)為null肛搬,Entry的引用也設(shè)為null,那么系統(tǒng)GC的時候自然會清理掉這塊內(nèi)存毕贼;
  2. Rehash until we encounter null: 這段就是掃描位置staleSlot之后温赔,null之前的Entry數(shù)組,清除每一個key為null的Entry鬼癣,同時若是key不為空让腹,做rehash远剩,調(diào)整其位置。

為什么要做rehash呢

因為我們在清理的過程中會把某個值設(shè)為null骇窍,那么這個值后面的區(qū)域如果之前是連著前面的瓜晤,那么下次循環(huán)查找時,就會只查到null為止腹纳。

舉個例子就是:...,<key1(hash1), value1>, <key2(hash1), value2>,...(即key1和key2的hash值相同) 此時痢掠,若插入<key3(hash2), value3>,其hash計算的目標位置被<key2(hash1), value2>占了嘲恍,于是往后尋找可用位置足画,hash表可能變?yōu)椋?..., <key1(hash1), value1>, <key2(hash1), value2>, <key3(hash2), value3>, ... 此時,若<key2(hash1), value2>被清理佃牛,顯然<key3(hash2), value3>應(yīng)該往前移(即通過rehash調(diào)整位置)淹辞,否則若以key3查找hash表,將會找不到key3

remove方法

刪除其實就是將 Entry 的鍵值設(shè)為 null俘侠,變?yōu)殛惻f的 Entry象缀。然后調(diào)用 expungeStaleEntry 清理陳舊的 Entry

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();
            expungeStaleEntry(i);
            return;
        }
    }
}

副本變量存取

ThreadLocal的set、get方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
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;
        }
    }
    return setInitialValue();
}

存取的基本流程就是首先獲得當(dāng)前線程的 ThreadLocalMap爷速,將 ThreadLocal 實例作為鍵值傳入 Map央星,然后就是進行相關(guān)的變量存取工作了。線程中的 ThreadLocalMap 是懶加載的惫东,只有真正的要存變量時才會調(diào)用 createMap 創(chuàng)建莉给,下面是 createMap 的實現(xiàn):

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果想要給 ThreadLocal 的副本變量設(shè)置初始值,需要重寫 initialValue 方法廉沮,如下面的形式:

ThreadLocal <Integer> threadLocal = new ThreadLocal() {
    protected Integer initialValue() {
        return 0;
    }
};

SuppliedThreadLocal

SuppliedThreadLocal是JDK8新增的內(nèi)部類颓遏,只是擴展了ThreadLocal的初始化值的方法而已,允許使用JDK8新增的Lambda表達式賦值滞时。需要注意的是州泊,函數(shù)式接口Supplier不允許為null

static final class SuppliedThreadLocal<T>extends ThreadLocal<T>{
    private final Supplier<?extends T> supplier;
     SuppliedThreadLocal(Supplier<?extends T> supplier){
       this.supplier= Objects.requireNonNull(supplier);
   }
     @Override
   protected T initialValue(){
       return supplier.get();
   }
}

總結(jié)

為什么會內(nèi)存泄漏

ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強引用來引用它漂洋,那么系統(tǒng) GC 的時候遥皂,這個ThreadLocal勢必會被回收,這樣一來刽漂,ThreadLocalMap中就會出現(xiàn)keynullEntry演训,就沒有辦法訪問這些keynullEntryvalue,如果當(dāng)前線程再遲遲不結(jié)束的話贝咙,這些keynullEntryvalue就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠無法回收样悟,造成內(nèi)存泄漏。

怎么避免內(nèi)存泄漏

每次使用完ThreadLocal,都調(diào)用它的remove()方法窟她,清除數(shù)據(jù)

為什么沒有next

ThreadLocalMap 中使用開放地址法來處理散列沖突陈症,而 HashMap 中使用的分離鏈表法。之所以采用不同的方式主要是因為:在 ThreadLocalMap 中的散列值分散的十分均勻震糖,很少會出現(xiàn)沖突录肯。并且 ThreadLocalMap 經(jīng)常需要清除無用的對象,使用純數(shù)組更加方便吊说。所以不需要next

為什么使用弱引用

因為如果這里使用普通的key-value形式來定義存儲結(jié)構(gòu)论咏,實質(zhì)上就會造成節(jié)點的生命周期與線程強綁定,只要線程沒有銷毀颁井,那么節(jié)點在GC分析中一直處于可達狀態(tài)厅贪,沒辦法被回收,而程序本身也無法判斷是否可以清理節(jié)點雅宾。弱引用是Java中四檔引用的第三檔养涮,比軟引用更加弱一些,如果一個對象沒有強引用鏈可達眉抬,那么一般活不過下一次GC贯吓。當(dāng)某個ThreadLocal已經(jīng)沒有強引用可達,則隨著它被垃圾回收吐辙,在ThreadLocalMap里對應(yīng)的Entry的鍵值會失效,這為ThreadLocalMap本身的垃圾清理提供了便利蘸劈。

如何清理entry

調(diào)用expungeStaleEntry進行實現(xiàn)

使用場景及方式

  • 主要應(yīng)用場景為按線程多實例(每個線程對應(yīng)一個實例)的對象的訪問昏苏,并且這個對象很多地方都要用到,例如數(shù)據(jù)庫連接威沫、Session管理贤惯、日志的uniqueID等

使用注意事項

  • ThreadLocal實例通常來說都是private static類型
  • ThreadLocal并未解決多線程訪問共享對象的問題,而是為每個線程創(chuàng)建一個單獨的變量副本棒掠,提供了保持對象的方法和避免參數(shù)傳遞的復(fù)雜性孵构;
  • ThreadLocal并不是每個線程拷貝一個對象,而是直接new(新建)一個烟很;
  • 如果ThreadLocal.set()的對象是多線程共享的颈墅,那么還是涉及并發(fā)問題。
  • 過度使用ThreadLocal很容易加大類之間的耦合度與依賴關(guān)系(開發(fā)過程可能會不得不過度考慮某個ThreadLocal在調(diào)用時是否已有值雾袱,存放的是哪個類放的什么值)
  • 應(yīng)用一定要自己負責(zé) remove恤筛,并且不要和線程池配合,因為woker線程往往是不會退出的芹橡。

參考地址

http://blog.zhangjikai.com/2017/03/29/%E3%80%90Java-%E5%B9%B6%E5%8F%91%E3%80%91%E8%AF%A6%E8%A7%A3-ThreadLocal
http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/
https://juejin.im/post/5a5efb1b518825732b19dca4#heading-9
https://www.cnblogs.com/micrari/p/6790229.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毒坛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌煎殷,老刑警劉巖屯伞,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異豪直,居然都是意外死亡劣摇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門顶伞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饵撑,“玉大人,你說我怎么就攤上這事唆貌』耍” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵锨咙,是天一觀的道長语卤。 經(jīng)常有香客問我,道長酪刀,這世上最難降的妖魔是什么粹舵? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮骂倘,結(jié)果婚禮上眼滤,老公的妹妹穿的比我還像新娘。我一直安慰自己历涝,他們只是感情好诅需,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荧库,像睡著了一般堰塌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上分衫,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天场刑,我揣著相機與錄音,去河邊找鬼蚪战。 笑死牵现,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的邀桑。 我是一名探鬼主播施籍,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼概漱!你這毒婦竟也來了丑慎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎竿裂,沒想到半個月后玉吁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡腻异,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年进副,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悔常。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡影斑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出机打,到底是詐尸還是另有隱情矫户,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布残邀,位于F島的核電站皆辽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏芥挣。R本人自食惡果不足惜驱闷,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望空免。 院中可真熱鬧空另,春花似錦、人聲如沸蹋砚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽都弹。三九已至娇豫,卻和暖如春匙姜,著一層夾襖步出監(jiān)牢的瞬間畅厢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工氮昧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留框杜,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓袖肥,卻偏偏與公主長得像咪辱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子椎组,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

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

  • 1. 概念 ThreadLocal 用于提供線程局部變量油狂,在多線程環(huán)境可以保證各個線程里的變量獨立于其它線程里的變...
    zly394閱讀 1,757評論 0 1
  • 前言 ThreadLocal很多同學(xué)都搞不懂是什么東西,可以用來干嘛。但面試時卻又經(jīng)常問到专筷,所以這次我和大家一起學(xué)...
    liangzzz閱讀 12,466評論 14 228
  • Android Handler機制系列文章整體內(nèi)容如下: Android Handler機制1之ThreadAnd...
    隔壁老李頭閱讀 7,645評論 4 30
  • ThreadLocal是一個關(guān)于創(chuàng)建線程局部變量的類弱贼。 通常情況下,我們創(chuàng)建的變量是可以被任何一個線程訪問并修改的...
    icecrea閱讀 726評論 0 2
  • 秋訊 也許磷蛹!你早應(yīng)該相信 大地上布滿了 明亮的波紋—— 它們像回聲一樣湮開 吞沒了濕濕的 苦顏色草葉 大口大口喘氣...
    愛亦如詩閱讀 306評論 0 1