国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Java 多線程(7): ThreadLocal 的應(yīng)用及原理

shadajin / 682人閱讀

摘要:但是還有另外的功能看的后一半代碼作用就是掃描位置之后的數(shù)組直到某一個(gè)為的位置,清除每個(gè)為的,所以使用可以降低內(nèi)存泄漏的概率。

在涉及到多線程需要共享變量的時(shí)候,一般有兩種方法:其一就是使用互斥鎖,使得在每個(gè)時(shí)刻只能有一個(gè)線程訪問該變量,好處就是便于編碼(直接使用 synchronized 關(guān)鍵字進(jìn)行同步訪問),缺點(diǎn)在于這增加了線程間的競爭,降低了效率;其二就是使用本文要講的 ThreadLocal。如果說 synchronized 是以“時(shí)間換空間”,那么 ThreadLocal 就是 “以空間換時(shí)間” —— 因?yàn)?ThreadLocal 的原理就是為每個(gè)線程都提供一個(gè)這樣的變量,使得這些變量是線程級(jí)別的變量,不同線程之間互不影響,從而達(dá)到可以并發(fā)訪問而不出現(xiàn)并發(fā)問題的目的。

首先我們來看一個(gè)客觀的事實(shí):當(dāng)一個(gè)可變對(duì)象被多個(gè)線程訪問時(shí),可能會(huì)得到非預(yù)期的結(jié)果 —— 所以先讓我們來看一個(gè)例子。在講到并發(fā)訪問的問題的時(shí)候,SimpleDateFormat 總是會(huì)被拿來當(dāng)成一個(gè)絕好的例子(從這點(diǎn)看感謝 JDK 提供了這么一個(gè)有設(shè)計(jì)缺陷的類方便我們當(dāng)成反面教材 :) )。因?yàn)?SimpleDateFormatformatparse 方法共享從父類 DateFormat 繼承而來的 Calendar 對(duì)象:

并且在 formatparse 方法中都會(huì)改變這個(gè) Calendar 對(duì)象:

format 方法片段:

parse 方法片段:

就拿 format 方法來說,考慮如下的并發(fā)情景:

線程A 此時(shí)調(diào)用 calendar.setTime(date1),然后 線程A 被中斷;

接著 線程B 執(zhí)行,然后調(diào)用 calendar.setTime(date2),然后 線程B 被中斷;

接著又是 線程A 執(zhí)行,但是此時(shí)的 calendar 已經(jīng)和之前的不一致了,所以便導(dǎo)致了并發(fā)問題。

所以因?yàn)檫@個(gè)共享的 calendar 對(duì)象,SimpleDateFormat 并不是一個(gè)線程安全的類,我們寫一段代碼來測(cè)試下。

(1)定義 DateFormatWrapper 類,來包裝對(duì) SimpleDateFormat 的調(diào)用:

public class DateFormatWrapper {

    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static String format(Date date) {
        return SDF.format(date);
    }

    public static Date parse(String str) throws ParseException {
        return SDF.parse(str);
    }
    
}

(2)然后寫一個(gè) DateFormatTest,開啟多個(gè)線程來使用 DateFormatWrapper

public class DateFormatTest {

    public static void main(String[] args) throws Exception {
        ExecutorService threadPool = Executors.newCachedThreadPool(); // 創(chuàng)建無大小限制的線程池

        List> futures = new ArrayList<>();

        for (int i = 0; i < 9; i++) {
            DateFormatTask task = new DateFormatTask();
            Future future = threadPool.submit(task); // 將任務(wù)提交到線程池

            futures.add(future);
        }

        for (Future future : futures) {
            try {
                future.get();
            } catch (ExecutionException ex) { // 運(yùn)行時(shí)如果出現(xiàn)異常則進(jìn)入 catch 塊
                System.err.println("執(zhí)行時(shí)出現(xiàn)異常:" + ex.getMessage());
            }
        }

        threadPool.shutdown();
    }

    static class DateFormatTask implements Callable {

        @Override
        public Void call() throws Exception {
            String str = DateFormatWrapper.format(
                    DateFormatWrapper.parse("2017-07-17 16:54:54"));
            System.out.printf("Thread(%s) -> %s
", Thread.currentThread().getName(), str);

            return null;
        }

    }
}

某次運(yùn)行的結(jié)果:

可以發(fā)現(xiàn),SimpleDateFormat 在多線程共享的情況下,不僅可能會(huì)出現(xiàn)結(jié)果錯(cuò)誤的情況,還可能會(huì)由于并發(fā)訪問導(dǎo)致運(yùn)行異常。當(dāng)然,我們肯定有解決的辦法:

DateFormatWrapperformatparse 方法加上 synchronized 關(guān)鍵字,壞處就是前面提到的這會(huì)加大線程間的競爭和切換而降低效率;

不使用全局的 SimpleDateFormat 對(duì)象,而是每次使用 formatparse 方法都新建一個(gè) SimpleDateFormat 對(duì)象,壞處也很明顯,每次調(diào)用 format 或者 parse 方法都要新建一個(gè) SimpleDateFormat,這會(huì)加大 GC 的負(fù)擔(dān);

使用 ThreadLocalThreadLocal 可以為每個(gè)線程提供一個(gè)獨(dú)立的 SimpleDateFormat 對(duì)象,創(chuàng)建的 SimpleDateFormat 對(duì)象個(gè)數(shù)最多和線程個(gè)數(shù)相同,相比于 (1),使用ThreadLocal不存在線程間的競爭;相比于 (2),使用ThreadLocal創(chuàng)建的 SimpleDateFormat 對(duì)象個(gè)數(shù)也更加合理(不會(huì)超過線程的數(shù)量)。

我們使用 ThreadLocal 來對(duì) DateFormatWrapper 進(jìn)行修改,使得每個(gè)線程使用多帶帶的 SimpleDateFormat

public class DateFormatWrapper {

    private static final ThreadLocal SDF = new ThreadLocal() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }

    };

    public static String format(Date date) {
        return SDF.get().format(date);
    }

    public static Date parse(String str) throws ParseException {
        return SDF.get().parse(str);
    }

}

如果使用 Java8,則初始化 ThreadLocal 對(duì)象的代碼可以改為:

private static final ThreadLocal SDF
            = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

然后再運(yùn)行 DateFormatTest,便始終是預(yù)期的結(jié)果:

我們已經(jīng)看到了 ThreadLocal 的功能,那 ThreadLocal 是如何實(shí)現(xiàn)為每個(gè)線程提供一份共享變量的拷貝呢?

在使用 ThreadLocal 時(shí),當(dāng)前線程訪問 ThreadLocal 中包含的變量是通過 get() 方法,所以首先來看這個(gè)方法的實(shí)現(xiàn):

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

通過代碼可以猜測(cè):

在某個(gè)地方(其實(shí)就是在 ThreadLocal 的內(nèi)部),JDK 實(shí)現(xiàn)了一個(gè)類似于 HashMap 的類,叫 ThreadLocalMap,該 “Map” 的鍵類型為 ThreadLocal,值類型為 T

然后每個(gè)線程都關(guān)聯(lián)著一個(gè) ThreadLocalMap 對(duì)象,并且可以通過 getMap(Thread t) 方法來獲得 線程t 關(guān)聯(lián)的 ThreadLocalMap 對(duì)象;

ThreadLocalMap 類有個(gè)以 ThreadLocal 對(duì)象為參數(shù)的 getEntry(ThreadLocal) 的方法,用來獲得當(dāng)前 ThreadLocal 對(duì)象關(guān)聯(lián)的 Entry 對(duì)象。一個(gè) Entry 對(duì)象就是一個(gè)鍵值對(duì),鍵(key)是 ThreadLocal 對(duì)象,值(value)是該 ThreadLocal 對(duì)象包含的變量(即 T)。

查看 getMap(Thread) 方法:

直接返回的就是 t.threadLocals,原來在 Thread 類中有一個(gè)就叫 threadLocalsThreadLocalMap 的變量:

所以每個(gè) Thread 都會(huì)擁有一個(gè) ThreadLocalMap 變量,來存放屬于該 Thread 的所有 ThreadLocal 變量。這樣來看的話,ThreadLocal就相當(dāng)于一個(gè)調(diào)度器,每次調(diào)用 get 方法的時(shí)候,都會(huì)先找到當(dāng)前線程的 ThreadLocalMap,然后再在這個(gè) ThreadLocalMap 中找到對(duì)應(yīng)的線程本地變量。

然后我們來看看當(dāng) mapnull(即第一次調(diào)用 get())時(shí)調(diào)用的 setInitialValue() 方法:

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

該方法首先會(huì)調(diào)用 initialValue() 方法來獲得該 ThreadLocal 對(duì)象中需要包含的變量 —— 所以這就是為什么使用 ThreadLocal 是需要繼承 ThreadLocal 時(shí)并覆寫 initialValue() 方法,因?yàn)檫@樣才能讓 setInitialValue() 調(diào)用 initialValue() 從而得到 ThreadLocal 包含的初始變量;然后就是當(dāng) map 不為 null 的時(shí)候,將該變量(value)與當(dāng)前ThreadLocal對(duì)象(this)在 map 中進(jìn)行關(guān)聯(lián);如果 mapnull,則調(diào)用 createMap 方法:

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

createMap 會(huì)調(diào)用 ThreadLocalMap 的構(gòu)造方法來創(chuàng)建一個(gè) ThreadLocalMap 對(duì)象:

可以看到該方法通過一個(gè) ThreadLocal 對(duì)象(firstKey)和該 ThreadLocal 包含的對(duì)象(firstValue)構(gòu)造了一個(gè) ThreadLocalMap 對(duì)象,使得該 map 在構(gòu)造完畢時(shí)候就包含了這樣一個(gè)鍵值對(duì)(firstKey -> firstValue)。

為啥需要使用 Map 呢?因?yàn)橐粋€(gè)線程可能有多個(gè) ThreadLocal 對(duì)象,可能是包含 SimpleDateFormat,也可能是包含一個(gè)數(shù)據(jù)庫連接 Connection,所以不同的變量需要通過對(duì)應(yīng)的 ThreadLocal 對(duì)象來快速查找 —— 那么 Map 當(dāng)然是最好的方式。

ThreadLocal 還提供了修改和刪除當(dāng)前包含對(duì)象的方法,修改的方法為 set,刪除的方法為 remove

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

很好理解,如果當(dāng)前 ThredLocal 還沒有包含值,那么就調(diào)用 createMap 來初始化當(dāng)前線程的 ThreadLocalMap 對(duì)象,否則直接在 map 中修改當(dāng)前 ThreadLocalthis)包含的值。

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

remove 方法就是獲得當(dāng)前線程的 ThreadLocalMap 對(duì)象,然后調(diào)用這個(gè) mapremove(ThreadLocal) 方法。查看 ThreadLocalMapremove(ThreadLocal) 方法的實(shí)現(xiàn):

邏輯就是先找到參數(shù)(ThreadLocal對(duì)象)對(duì)應(yīng)的 Entry,然后調(diào)用 Entryclear() 方法,再調(diào)用 expungeStaleEntry(i)i 為該 EntrymapEntry 數(shù)組中的索引。

(1)首先來看看 e.clear() 做了什么。

查看 ThreadLocalMap 的源代碼,我們可以發(fā)現(xiàn)這個(gè) “Map” 的 Entry 的實(shí)現(xiàn)如下:

可以看到,該 Entry 類繼承自 WeakReference>,所以 Entry 是一個(gè) WeakReference(弱引用),而且該 WeakReference 包含的是一個(gè) ThreadLocal 對(duì)象 —— 因而每個(gè) Entry 是一個(gè)弱引用的 ThreadLocal 對(duì)象(又因?yàn)?Entry 包括了一個(gè) value 變量,所以該 Entry 構(gòu)成了一個(gè) ThreadLocal -> Object 的鍵值對(duì)),而 Entryclear() 方法,是繼承自 WeakReference,作用就是將 WeakReference 包含的對(duì)象的引用設(shè)置為 null

我們知道對(duì)于一個(gè)弱引用的對(duì)象,一旦該對(duì)象不再被其他對(duì)象引用(比如像 clear() 方法那樣將對(duì)象引用直接設(shè)置為 null),那么在 GC 發(fā)生的時(shí)候,該對(duì)象便會(huì)被 GC 回收。所以讓 Entry 作為一個(gè) WeakReference,配合 ThreadLocalremove 方法,可以及時(shí)清除某個(gè) Entry 中的 ThreadLocalEntrykey)。

(2)expungeStaleEntry(i)的作用

先來看 expungeStaleEntry 的前一半代碼:

expungeStaleEntry 這部分代碼的作用就是將 i 位置上的 Entryvalue 設(shè)置為 null,以及將 Entry 的引用設(shè)置為 null。為什么要這做呢?因?yàn)榍懊嬲{(diào)用 e.clear(),只是將 Entrykey 設(shè)置為 null 并且可以使其在 GC 是被快速回收,但是 Entryvalue 在調(diào)用 e.clear() 后并不會(huì)為 null —— 所以如果不對(duì) value 也進(jìn)行清除,那么就可能會(huì)導(dǎo)致內(nèi)存泄漏了。因此expungeStaleEntry 方法的一個(gè)作用在于可以把需要清除的 Entry 徹底的從 ThreadLocalMap 中清除(keyvalueEntry 全部設(shè)置為 null)。但是 expungeStaleEntry 還有另外的功能:看 expungeStaleEntry 的后一半代碼:

作用就是掃描位置 staleSlot 之后的 Entry 數(shù)組(直到某一個(gè)為 null 的位置),清除每個(gè) keyThreadLocal) 為 nullEntry,所以使用 expungeStaleEntry 可以降低內(nèi)存泄漏的概率。但是如果某些 ThreadLocal 變量不需要使用但是卻沒有調(diào)用到 expungeStaleEntry 方法,那么就會(huì)導(dǎo)致這些 ThreadLocal 變量長期的貯存在內(nèi)存中,引起內(nèi)存浪費(fèi)或者泄露 —— 所以,如果確定某個(gè) ThreadLocal 變量已經(jīng)不需要使用,需要及時(shí)的使用 ThreadLocalremove() 方法(ThreadLocalgetset 方法也會(huì)調(diào)用到 expungeStaleEntry),將其從內(nèi)存中清除。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/67412.html

相關(guān)文章

  • 想進(jìn)大廠?50個(gè)線程面試題,你會(huì)少?(一)

    摘要:下面是線程相關(guān)的熱門面試題,你可以用它來好好準(zhǔn)備面試。線程安全問題都是由全局變量及靜態(tài)變量引起的。持有自旋鎖的線程在之前應(yīng)該釋放自旋鎖以便其它線程可以獲得自旋鎖。 最近看到網(wǎng)上流傳著,各種面試經(jīng)驗(yàn)及面試題,往往都是一大堆技術(shù)題目貼上去,而沒有答案。 不管你是新程序員還是老手,你一定在面試中遇到過有關(guān)線程的問題。Java語言一個(gè)重要的特點(diǎn)就是內(nèi)置了對(duì)并發(fā)的支持,讓Java大受企業(yè)和程序員...

    wow_worktile 評(píng)論0 收藏0
  • ThreadLocal基本原理運(yùn)用

    摘要:基本原理線程本地變量是和線程相關(guān)的變量,一個(gè)線程則一份數(shù)據(jù)。其中為聲明的對(duì)象。對(duì)于一個(gè)對(duì)象倘若沒有成員變量,單例非常簡單,不用去擔(dān)心多線程同時(shí)對(duì)成員變量修改而產(chǎn)生的線程安全問題。并且還不能使用單例模式,因?yàn)槭遣荒芏嗑€程訪問的。 ThreadLocal簡述 下面我們看一下ThreadLocal類的官方注釋。 This class provides thread-local variab...

    VEIGHTZ 評(píng)論0 收藏0
  • Android 進(jìn)階

    摘要:理解內(nèi)存模型對(duì)多線程編程無疑是有好處的。干貨高級(jí)動(dòng)畫高級(jí)動(dòng)畫進(jìn)階,矢量動(dòng)畫。 這是最好的Android相關(guān)原創(chuàng)知識(shí)體系(100+篇) 知識(shí)體系從2016年開始構(gòu)建,所有的文章都是圍繞著這個(gè)知識(shí)體系來寫,目前共收入了100多篇原創(chuàng)文章,其中有一部分未收入的文章在我的新書《Android進(jìn)階之光》中。最重要的是,這個(gè)知識(shí)體系仍舊在成長中。 Android 下拉刷新庫,這一個(gè)就夠了! 新鮮出...

    DoINsiSt 評(píng)論0 收藏0
  • Java面試 32個(gè)核心必考點(diǎn)完全解析

    摘要:如問到是否使用某框架,實(shí)際是是問該框架的使用場(chǎng)景,有什么特點(diǎn),和同類可框架對(duì)比一系列的問題。這兩個(gè)方向的區(qū)分點(diǎn)在于工作方向的側(cè)重點(diǎn)不同。 [TOC] 這是一份來自嗶哩嗶哩的Java面試Java面試 32個(gè)核心必考點(diǎn)完全解析(完) 課程預(yù)習(xí) 1.1 課程內(nèi)容分為三個(gè)模塊 基礎(chǔ)模塊: 技術(shù)崗位與面試 計(jì)算機(jī)基礎(chǔ) JVM原理 多線程 設(shè)計(jì)模式 數(shù)據(jù)結(jié)構(gòu)與算法 應(yīng)用模塊: 常用工具集 ...

    JiaXinYi 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<