JAVA 多線程與高并發(fā)學習筆記(四)——ThreadLocal

在 Java 多線程中仲锄,為了保證多個線程對變量的安全訪問拓型,可以將變量放到 ThreadLocal 類型的對象中如失,使變量在每個線程中都有獨立值。ThreadLocal 類通常被翻譯為線程本地變量或線程局部變量策彤。

ThreadLocal 的基本使用

ThreadLocal 中主要包含如下方法:

  • set(T value),設置當前線程在線程本地變量實例中綁定的值匣摘。
  • T get()店诗,獲取當前線程在線程本地變量實例中綁定的值。
  • remove()音榜,移除當前線程在線程本地變量實例中綁定的值必搞。

看個簡單的例子:

public class ThreadLocalDemo {

    static class Foo {
        static final AtomicInteger AMOUNT = new AtomicInteger(0);
        int index = 0;
        int bar = 10;
        public Foo() {
            index = AMOUNT.incrementAndGet();
        }

        public int getBar() {
            return bar;
        }

        public void setBar(int bar) {
            this.bar = bar;
        }

        @Override
        public String toString() {
            return index + "@Foo{bar=" + bar + "}";
        }
    }

    private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    if (LOCAL_FOO.get() == null) {
                        LOCAL_FOO.set(new Foo());
                    }
                    System.out.println("線程的初始本地值: " + LOCAL_FOO.get());
                    try {
                        for (int i = 0; i < 3; i++) {
                            Foo foo = LOCAL_FOO.get();
                            foo.setBar(foo.getBar() + 1);
                            Thread.sleep(10);
                        }

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("最終線程值: " + LOCAL_FOO.get());
                    LOCAL_FOO.remove();

                }
            });
        }
    }
}

運行結果如下:

threadlocal-example.png

如果第一次不想判斷是否為空,還可以使用如下方式在定義的時候設置初始值囊咏。

ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());

ThreadLocal的使用場景

ThreadLocal是解決線程安全問題一個較好的方案恕洲,它通過為每個線程提供一個獨立的值去解決并發(fā)的沖突問題。

線程隔離

ThreadLocal 主要用于線程隔離梅割,防止自己的變量被其它線程修改霜第,而且可以避免同步鎖帶來的性能損失。

ThreadLocal 線程隔離的使用案例包括為每個線程綁定一個用戶會話信息户辞、數(shù)據(jù)庫連接泌类、HTTP請求等。常見使用場景為數(shù)據(jù)庫連接獨享底燎、Session數(shù)據(jù)管理等刃榨。

看一段 Hibernate 中的代碼:

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if(s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

跨函數(shù)傳遞數(shù)據(jù)

通常用于一個線程內(nèi),跨類双仍、跨方法傳遞數(shù)據(jù)時枢希,在某一個地方進行了設置,在隨后同一個線程的任意地方都可以獲取朱沃。

典型應用包括:

  • 用來傳遞請求過程中的用戶ID苞轿。
  • 用來傳遞請求過程中的用戶會話茅诱。
  • 用來傳遞HTTP用戶的請求實例的 HttpRequest

下面是一段參考代碼:

public class SessionHolder {
    private static final ThreadLocal<String> sidLocal = new ThreadLocal<>("sidLocal");

    private static final ThreadLocal<UserDTO> sessionUserLocal = new ThreadLocal<>("sessionUserLocal");

    private static final ThreadLocal<HttpSession> sessionLocal = new ThreadLocal<>("sessionLocal");

    ...

    public static void setSession(HttpSession session) {
        sessionLocal.set(session);
    }

    public static HttpSession getSession() {
        HttpSession session = sessionLocal.get();
        Assert.notNull(session, "session not set");
        return session;
    }
}

ThreadLocal 源碼分析

本部分看下 ThreadLocal 源碼搬卒。

set方法

set(T value)方法用于設置本地線程變量瑟俭,核心源碼如下:

public void set(T value) {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 獲取當前線程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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

大致流程:

  1. 獲取當前線程,并獲取當前線程的 ThreadLocalMap 成員傳入 map契邀。
  2. 如果 map 不為空摆寄,就將 value 設置到 map 中,當前的 ThreadLocal 作為鍵坯门。
  3. 如果 map 為空椭迎,就位該線程創(chuàng)建 map,然后將 value 加入其中田盈。

get方法

get() 方法源碼如下:

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 獲取線程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果map不為空
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map為空
    return setInitialValue();
}

// 設置 ThreadLocal 關聯(lián)的初始值并返回
private T setInitialValue() {
    // 獲取初始值
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

大致流程如下:

  1. 獲取當前線程畜号,并獲取當前線程的 ThreadLocalMap 成員,暫存于 map 變量允瞧。
  2. 如果獲得的 map 不為空简软,那么以當前的 ThreadLocal 實例為鍵獲取 map 中的記錄。
  3. 如果記錄的值不為空述暂,就返回該值痹升。
  4. 如果記錄為空,就利用 initialValue() 初始化鉤子函數(shù)獲取初始值畦韭,被設置在 map 中疼蛾。

remove 方法

remove() 方法用于移除線程本地變量中的值,源碼如下:

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

initialValue 方法

initialValue() 方法用于獲取初始值艺配,源碼如下:

protected T initialValue() {
    return null;
}

默認返回 null察郁,一般使用 ThreadLocal.withInitial(...) 靜態(tài)工廠方法來在定義 ThreadLocal 實例時設置初始值的回調(diào)函數(shù)。

靜態(tài)工廠方法源碼如下:

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

使用示例:

ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());

ThreadLocalMap 源碼分析

ThreadLocal 的操作都是基于 ThreadLocalMap 展開的转唉,而 ThreadLocalMapThreadLocal 的一個內(nèi)部靜態(tài)類皮钠,實現(xiàn)了一套簡單的 Map 結構。

ThreadLocalMap 的成員變量如下:

static class ThreadLocalMap {
    // 條目類型赠法,一個靜態(tài)內(nèi)部類
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    // 初始容量
    private static final int INITIAL_CAPACITY = 16;

    // 條目數(shù)組麦轰,作為哈希表使用
    private Entry[] table;

    // 當前條目數(shù)量
    private int size = 0;

    // 擴容因子
    private int threshold; // Default to 0
    ...
    

然后看一下它的 set 方法。

 private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    // 根據(jù) key 的 HashCode 找到插槽
    int i = key.threadLocalHashCode & (len-1);

    // 從i開始向后循環(huán)搜索
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        // 找到插槽
        if (k == key) {
            e.value = value;
            return;
        }

        // 找到異常插槽砖织,重新設置
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 沒有找到插槽款侵,增加新的 Entry
    tab[i] = new Entry(key, value);
    // 數(shù)量增加
    int sz = ++size;
    // 清理 key 為 null 的無效 Entry
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

另外,需要注意 Entry 使用了弱引用(WeakReference)侧纯。

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

這里簡單介紹下原因新锈。首先介紹下弱引用的特點,僅有弱引用指向的對象只能生存到下一次垃圾回收之前茂蚓。因此壕鹉,在下次 GC 發(fā)生時剃幌,就可以使用那些沒有被其他強引用指向聋涨、僅被 EntryKey 所指向的 ThreadLocal 實例能被順利回收晾浴。回收之后牍白,EntryKey 變?yōu)榭占够耍罄m(xù)調(diào)用 get()set()remove() 的時候會清空這些 Entry茂腥。

ThreadLocal 使用規(guī)范

在如下條件下狸涌,ThreadLocal 有可能發(fā)生內(nèi)存泄露:

  • 線程長時間運行而沒有被銷毀,線程池中的 Thread 實例很容易滿足此條件最岗。
  • ThreadLocal 引用內(nèi)設置為null帕胆,且后續(xù)在同一 Thread 實例執(zhí)行期間,沒有發(fā)生其它 ThreadLocal 實例的get()般渡、set()remove()操作懒豹。

建議使用 ThreadLocal 時遵循以下原則:

  • 盡量使用 private static final 修飾 ThreadLocal 實例,privatefinal修飾符保證盡可能不讓他人修改驯用,static修飾符保證實例全局唯一脸秽。
  • ThreadLocal 使用完成之后務必調(diào)用 remove() 方法。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝴乔,一起剝皮案震驚了整個濱河市记餐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌薇正,老刑警劉巖片酝,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異挖腰,居然都是意外死亡钠怯,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門曙聂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晦炊,“玉大人,你說我怎么就攤上這事宁脊《瞎” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵榆苞,是天一觀的道長稳衬。 經(jīng)常有香客問我,道長坐漏,這世上最難降的妖魔是什么薄疚? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任碧信,我火速辦了婚禮,結果婚禮上街夭,老公的妹妹穿的比我還像新娘砰碴。我一直安慰自己,他們只是感情好板丽,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布呈枉。 她就那樣靜靜地躺著,像睡著了一般埃碱。 火紅的嫁衣襯著肌膚如雪猖辫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天砚殿,我揣著相機與錄音啃憎,去河邊找鬼。 笑死似炎,一個胖子當著我的面吹牛辛萍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播名党,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼叹阔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了传睹?” 一聲冷哼從身側(cè)響起耳幢,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎欧啤,沒想到半個月后睛藻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡邢隧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年店印,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倒慧。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡按摘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纫谅,到底是詐尸還是另有隱情炫贤,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布付秕,位于F島的核電站兰珍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏询吴。R本人自食惡果不足惜掠河,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一亮元、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧唠摹,春花似錦爆捞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毛好。三九已至望艺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肌访,已是汗流浹背找默。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吼驶,地道東北人惩激。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像蟹演,于是被迫代替她去往敵國和親风钻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355