Java并發(fā)編程 ThreadLocal

1.ThreadLocal的用途

場(chǎng)景1:
每個(gè)線程需要一個(gè)獨(dú)享的對(duì)象(通常是工具類淑仆,典型需要使用的類有SimpleDateFormat和Random)

看問(wèn)題代碼

打印1000個(gè)不同線程

public class ThreadLocalNormalUsage03 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //參數(shù)的單位是毫秒,從1970.1.1 00:00:00 GMT計(jì)時(shí)
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}
image.png
所用的線程都共用同一個(gè)SimpleDateFormat對(duì)象均驶,發(fā)生了線程安全問(wèn)題救湖。
解決
public class ThreadLocalNormalUsage05 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }

    public String date(int seconds) {
        //參數(shù)的單位是毫秒,從1970.1.1 00:00:00 GMT計(jì)時(shí)
        Date date = new Date(1000 * seconds);
//        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormatter {
    /**
     * 寫(xiě)法一
     */
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    /**
     * Lambda表達(dá)式
     */
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

場(chǎng)景2:
每個(gè)線程內(nèi)需要保存全局變量(例如在攔截器中獲取用戶信息)艾疟,可以讓不同方法直接使用押框,避免參數(shù)傳遞的麻煩
實(shí)例:當(dāng)前用戶信息需要被線程內(nèi)所有方法共享

public class ThreadLocalNormalUsage06 {

    public static void main(String[] args) {
        new Service1().process("kpioneer");
        new Service2().process();
        new Service3().process();
    }
}

class Service1 {
    public void process(String name) {
        User user = new User(name);
        UserContextHolder.holder.set(user);
        System.out.println("Service1設(shè)置用戶名:" + user.name);
    }
}

class Service2 {

    public void process() {
        User user = UserContextHolder.holder.get();
        ThreadSafeFormatter.dateFormatThreadLocal.get();
        System.out.println("Service2拿到用戶名:" + user.name);
    }
}

class Service3 {

    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用戶名:" + user.name);
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}
Service1設(shè)置用戶名:kpioneer
Service2拿到用戶名:kpioneer
Service3拿到用戶名:kpioneer

使用TreadLocal,這樣無(wú)需synchronized尿背,可以在不影響性能的情況下端仰,也無(wú)需層層傳遞參數(shù),就可以達(dá)到保存當(dāng)前線程對(duì)應(yīng)的用戶信息的目的田藐。

2. ThreadLocal的兩個(gè)作用

  1. 讓某個(gè)需要用的對(duì)象在線程間隔離(每個(gè)線程都有自己的獨(dú)立的對(duì)象)
  2. 在任何方法中都可以輕松獲取到該對(duì)象荔烧。
場(chǎng)景一:initialValue

在ThreadLocal第一次get的時(shí)候把對(duì)象給初始化出來(lái),對(duì)象的初始化時(shí)機(jī)可以由我們控制

場(chǎng)景二:set

如果需要保存到ThreadLocal里的對(duì)象的生成時(shí)機(jī)不由我們隨意控制汽久,例如攔截器生成的用戶信息茴晋,用ThreadLocal.set直接放到我們的ThreadLocal中去,以便后續(xù)使用回窘。

3.主要方法介紹

initialValue()
  • 該方法會(huì)返回當(dāng)前線程對(duì)應(yīng)的“初始值”诺擅,這是一個(gè)延遲加載的方法,只有在調(diào)用get的時(shí)候啡直,才會(huì)觸發(fā)烁涌。
  • 當(dāng)線程第一次使用get方法訪問(wèn)變量時(shí)苍碟,將調(diào)用此方法,除非線程先前調(diào)用了set方法撮执,在這** 種情況下微峰,不會(huì)為線程調(diào)用本initialValue方法
  • 通常,每個(gè)線程最多調(diào)用一次此方法抒钱,但如果調(diào)用了remove()后蜓肆,再調(diào)用get(),則可以再次調(diào)用此方法
  • 如果不重寫(xiě)本方法,這個(gè)方法會(huì)返回null谋币。
void set(T t)

設(shè)置當(dāng)前線程的threadLocal變量為指定值.

get

返回當(dāng)前線程的這個(gè)threadLocal變量的映射值. 如果變量沒(méi)有當(dāng)前線程的值,它第一次初始化的值是由initialValue方法的調(diào)用返回

    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();
    }
remove 移除當(dāng)前線程的threadLocal值
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

4. 原理仗扬、源碼分析

  • 每個(gè)Thread對(duì)象都持有一個(gè)ThreadLocalMap成員變量
  • ThreadLocalMap存放ThreadLocal對(duì)象為key,initialValue對(duì)象或者set對(duì)象為value
  • ThreadLocalMap存多個(gè)ThreadLocal
public class Thread implements Runnable {
     ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
     ...
}
    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;
    }

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
ThreadLocalMap的底層源碼分析

ThreadLocalMap是ThreadLocal內(nèi)部的一個(gè)Map實(shí)現(xiàn)蕾额,然而它沒(méi)有實(shí)現(xiàn)任何集合的接口規(guī)范早芭,因?yàn)樗鼉H供ThreadLocal內(nèi)部使用,數(shù)據(jù)結(jié)構(gòu)采用數(shù)組+開(kāi)方地址法(線性探測(cè)法)诅蝶,Entry繼承WeakRefrence退个,是基于ThreadLocal這種特殊場(chǎng)景實(shí)現(xiàn)的Map,它的實(shí)現(xiàn)方式很值得我們?nèi)⊙芯浚?/p>

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

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

源碼分析:

1.Entry中key只能是ThreadLocal對(duì)象调炬,被規(guī)定死了的

2.Entry繼承了WeakRefrence(弱引用语盈,生存周期只能活到下次GC前),但是只有Key是弱引用缰泡,Value并不是弱引用

ps:value既然不是弱引用刀荒,那么key在被回收之后(key=null)Value并沒(méi)有被回收,如果當(dāng)前線程被回收了那還好匀谣,這樣value也和線程一起被回收了照棋,要是當(dāng)前線程是線程池這樣的環(huán)境资溃,線程結(jié)束沒(méi)有銷毀回收武翎,那么Value永遠(yuǎn)不會(huì)被回收,當(dāng)存在大量這樣的value的時(shí)候溶锭,就會(huì)產(chǎn)生內(nèi)存泄漏宝恶,那么Java 8中如何解決這個(gè)問(wèn)題的呢?

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


            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

以上是ThreadLocalMap的set方法趴捅,for循環(huán)遍歷整個(gè)Entry數(shù)組垫毙,遇到key=null的就會(huì)替換,這樣就不存在value內(nèi)存泄漏的問(wèn)題了拱绑!

ThreaLocalMap中key的HashCode計(jì)算

ThreadLocalMap這里采用的是線性探測(cè)法综芥,也就是如果發(fā)生沖突,就繼續(xù)找下一個(gè)空位置猎拨,而不是用鏈表拉鏈法膀藐。
ThreaLocalMap的key是ThreaLocal屠阻,它不會(huì)傳統(tǒng)的調(diào)用ThreadLocal的hashcode方法(繼承自object的hashcode)宿崭,而是調(diào)用nexthashcode

private final int threadLocalHashCode = nextHashCode();

 private static AtomicInteger nextHashCode = new AtomicInteger();

 //1640531527 這是一個(gè)神奇的數(shù)字穿香,能夠讓hash槽位分布相當(dāng)均勻
 private static final int HASH_INCREMENT = 0x61c88647; 

 private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
 }
private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode計(jì)算槽位
    // hash沖突時(shí),使用開(kāi)放地址法
    // 因?yàn)楠?dú)特和hash算法又憨,導(dǎo)致hash沖突很少虾啦,一般不會(huì)走進(jìn)這個(gè)for循環(huán)
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) { // key 相同麻诀,則覆蓋value
            e.value = value; 
            return;
        }

        if (k == null) { // key = null,說(shuō)明 key 已經(jīng)被回收了傲醉,進(jìn)入替換方法
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些過(guò)期的值蝇闭,并判斷是否需要擴(kuò)容
        rehash(); // 擴(kuò)容
}

源碼分析:

1.先是計(jì)算槽位

2.Entry數(shù)組中存在需要插入的key,直接替換即可需频,存在key=null丁眼,也是替換(可以避免value內(nèi)存泄漏)

3.Entry數(shù)組中不存在需要插入的key,也沒(méi)有key=null昭殉,新增一個(gè)Entry苞七,然后判斷一下需不需要擴(kuò)容和清除過(guò)期的值

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key) // 無(wú)hash沖突情況
        return e;
    else
        return getEntryAfterMiss(key, i, e); // 有hash沖突情況
}

1.計(jì)算槽位i,判斷table[i]是否有目標(biāo)key挪丢,沒(méi)有(hahs沖突了)則進(jìn)入getEntryAfterMiss方法

5. 注意事項(xiàng)

如何避免內(nèi)存泄露

調(diào)用remove方法蹂风,就會(huì)刪除對(duì)應(yīng)的Entry對(duì)象,可以避免內(nèi)存泄露乾蓬,所以使用完ThreadLocal之后惠啄,應(yīng)該主動(dòng)調(diào)用remove方法。

空指針異常
public class ThreadLocalNPE {

    ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    public long get() {
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println(threadLocalNPE.get());
            }
        });
        thread1.start();
    }
}

Exception in thread "main" java.lang.NullPointerException
    at com.kpioneer.thread.threadlocal.ThreadLocalNPE.get(ThreadLocalNPE.java:15)
    at com.kpioneer.thread.threadlocal.ThreadLocalNPE.main(ThreadLocalNPE.java:20)

因?yàn)閘ong是基本類型任内,這里threadLocalNPE.get()返回值null撵渡,且類型是Long為包裝類型,在拆箱過(guò)程中轉(zhuǎn)化為long類型死嗦,報(bào)空指針錯(cuò)誤趋距。

共享對(duì)象

如果在每個(gè)線程中ThreadLocal.set()進(jìn)去的東西本來(lái)就是多線程共享的同一個(gè)對(duì)象,比如static對(duì)象越除,那么多個(gè)線程的ThreadLocal.get()取得的還是這個(gè)共享對(duì)象本身节腐,還是有并發(fā)訪問(wèn)問(wèn)題。

6. 其它

  • 如果可以不使用ThreadLocal就解決問(wèn)題摘盆,那么不要強(qiáng)行使用
    例如在任務(wù)數(shù)很少時(shí)翼雀,在局部變量中可以新建對(duì)象就可以解決問(wèn)題,那么就不需要使用到ThreadLocal
  • 優(yōu)先使用框架的支持孩擂,而不是自己創(chuàng)造
    例如在Spring中狼渊,如果可以使用RequestContextHolder,那么就不需要自己維護(hù)ThreadLocal类垦,因?yàn)樽约嚎赡軙?huì)忘記調(diào)用remove()方法等狈邑,造成內(nèi)存泄露坦弟。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市官地,隨后出現(xiàn)的幾起案子酿傍,更是在濱河造成了極大的恐慌,老刑警劉巖驱入,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赤炒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡亏较,警方通過(guò)查閱死者的電腦和手機(jī)莺褒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雪情,“玉大人遵岩,你說(shuō)我怎么就攤上這事⊙餐ǎ” “怎么了尘执?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)宴凉。 經(jīng)常有香客問(wèn)我誊锭,道長(zhǎng),這世上最難降的妖魔是什么弥锄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任丧靡,我火速辦了婚禮,結(jié)果婚禮上籽暇,老公的妹妹穿的比我還像新娘温治。我一直安慰自己,他們只是感情好戒悠,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布熬荆。 她就那樣靜靜地躺著,像睡著了一般救崔。 火紅的嫁衣襯著肌膚如雪惶看。 梳的紋絲不亂的頭發(fā)上捏顺,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天六孵,我揣著相機(jī)與錄音,去河邊找鬼幅骄。 笑死劫窒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拆座。 我是一名探鬼主播主巍,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼冠息,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了孕索?” 一聲冷哼從身側(cè)響起逛艰,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搞旭,沒(méi)想到半個(gè)月后散怖,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肄渗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年镇眷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翎嫡。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡欠动,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惑申,到底是詐尸還是另有隱情具伍,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布圈驼,位于F島的核電站沿猜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏碗脊。R本人自食惡果不足惜啼肩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望衙伶。 院中可真熱鬧祈坠,春花似錦、人聲如沸矢劲。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)芬沉。三九已至躺同,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丸逸,已是汗流浹背蹋艺。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留黄刚,地道東北人捎谨。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親涛救。 傳聞我的和親對(duì)象是個(gè)殘疾皇子畏邢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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