本節(jié),我們來探討一個特殊的概念,線程本地變量,在Java中的實現(xiàn)是類ThreadLocal,它是什么?有什么用?實現(xiàn)原理是什么?讓我們接下來逐步探討。

基本概念和用法

線程本地變量是說,每個線程都有同一個變量的獨有拷貝,這個概念聽上去比較難以理解,我們先直接來看類TheadLocal的用法。

ThreadLocal是一個泛型類,接受一個類型參數(shù)T,它只有一個空的構(gòu)造方法,有兩個主要的public方法:

public T get()public void set(T value)

set就是設(shè)置值,get就是獲取值,如果沒有值,返回null,看上去,ThreadLocal就是一個單一對象的容器,比如:

public static void main(String[] args) {
    ThreadLocal<Integer> local = new ThreadLocal<>();
    local.set(100);
    System.out.println(local.get());
}

輸出為100。

那ThreadLocal有什么特殊的呢?特殊發(fā)生在有多個線程的時候,看個例子:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

public class ThreadLocalBasic {    static ThreadLocal<Integer> local = new ThreadLocal<>();    public static void main(String[] args) throws InterruptedException {
        Thread child = new Thread() {
            @Override            public void run() {
                System.out.println("child thread initial: " + local.get());
                local.set(200);
                System.out.println("child thread final: " + local.get());
            }
        };
        local.set(100);
        child.start();
        child.join();
        System.out.println("main thread final: " + local.get());
    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

local是一個靜態(tài)變量,main方法創(chuàng)建了一個子線程child,main和child都訪問了local,程序的輸出為:

child thread initial: nullchild thread final: 200main thread final: 100

這說明,main線程對local變量的設(shè)置對child線程不起作用,child線程對local變量的改變也不會影響main線程,它們訪問的雖然是同一個變量local,但每個線程都有自己的獨立的值,這就是線程本地變量的含義。

除了get/set,ThreadLocal還有兩個方法:

protected T initialValue()public void remove()

initialValue用于提供初始值,它是一個受保護方法,可以通過匿名內(nèi)部類的方式提供,當(dāng)調(diào)用get方法時,如果之前沒有設(shè)置過,會調(diào)用該方法獲取初始值,默認實現(xiàn)是返回null。remove刪掉當(dāng)前線程對應(yīng)的值,如果刪掉后,再次調(diào)用get,會再調(diào)用initialValue獲取初始值。看個簡單的例子:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

public class ThreadLocalInit {    static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){

        @Override        protected Integer initialValue() {            return 100;
        }
    };    public static void main(String[] args) {
        System.out.println(local.get());
        local.set(200);
        local.remove();
        System.out.println(local.get());
    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

輸出值都是100。

使用場景

ThreadLocal有什么用呢?我們來看幾個例子。

DateFormat/SimpleDateFormat

ThreadLocal是實現(xiàn)線程安全的一種方案,比如對于DateFormat/SimpleDateFormat,我們在32節(jié)介紹過日期和時間操作,提到它們是非線程安全的,實現(xiàn)安全的一種方式是使用鎖,另一種方式是每次都創(chuàng)建一個新的對象,更好的方式就是使用ThreadLocal,每個線程使用自己的DateFormat,就不存在安全問題了,在線程的整個使用過程中,只需要創(chuàng)建一次,又避免了頻繁創(chuàng)建的開銷,示例代碼如下:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

public class ThreadLocalDateFormat {    static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() {

        @Override        protected DateFormat initialValue() {            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };    public static String date2String(Date date) {        return sdf.get().format(date);
    }    public static Date string2Date(String str) throws ParseException {        return sdf.get().parse(str);
    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

需要說明的是,ThreadLocal對象一般都定義為static,以便于引用。

ThreadLocalRandom

即使對象是線程安全的,使用ThreadLocal也可以減少競爭,比如,我們在34節(jié)介紹過Random類,Random是線程安全的,但如果并發(fā)訪問競爭激烈的話,性能會下降,所以Java并發(fā)包提供了類ThreadLocalRandom,它是Random的子類,利用了ThreadLocal,它沒有public的構(gòu)造方法,通過靜態(tài)方法current獲取對象,比如:

public static void main(String[] args) {
    ThreadLocalRandom rnd = ThreadLocalRandom.current();
    System.out.println(rnd.nextInt());
}

current方法的實現(xiàn)為:

public static ThreadLocalRandom current() {    return localRandom.get();
}

localRandom就是一個ThreadLocal變量:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

private static final ThreadLocal<ThreadLocalRandom> localRandom =    new ThreadLocal<ThreadLocalRandom>() {        protected ThreadLocalRandom initialValue() {            return new ThreadLocalRandom();
        }
};

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

上下文信息

ThreadLocal的典型用途是提供上下文信息,比如在一個Web服務(wù)器中,一個線程執(zhí)行用戶的請求,在執(zhí)行過程中,很多代碼都會訪問一些共同的信息,比如請求信息、用戶身份信息、數(shù)據(jù)庫連接、當(dāng)前事務(wù)等,它們是線程執(zhí)行過程中的全局信息,如果作為參數(shù)在不同代碼間傳遞,代碼會很啰嗦,這時,使用ThreadLocal就很方便,所以它被用于各種框架如Spring中,我們看個簡單的示例:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

public class RequestContext {    public static class Request { //...    };    private static ThreadLocal<String> localUserId = new ThreadLocal<>();    private static ThreadLocal<Request> localRequest = new ThreadLocal<>();    public static String getCurrentUserId() {        return localUserId.get();
    }    public static void setCurrentUserId(String userId) {
        localUserId.set(userId);
    }    public static Request getCurrentRequest() {        return localRequest.get();
    }    public static void setCurrentRequest(Request request) {
        localRequest.set(request);
    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

在首次獲取到信息時,調(diào)用set方法如setCurrentRequest/setCurrentUserId進行設(shè)置,然后就可以在代碼的任意其他地方調(diào)用get相關(guān)方法進行獲取了。

基本實現(xiàn)原理

ThreadLocal是怎么實現(xiàn)的呢?為什么對同一個對象的get/set,每個線程都能有自己獨立的值呢?我們直接來看代碼。

set方法的代碼為:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

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

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

它調(diào)用了getMap,getMap的代碼為:

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

返回線程的實例變量threadLocals,它的初始值為null,在null時,set調(diào)用createMap初始化,代碼為:

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

從以上代碼可以看出,每個線程都有一個Map,類型為ThreadLocalMap,調(diào)用set實際上是在線程自己的Map里設(shè)置了一個條目,鍵為當(dāng)前的ThreadLocal對象,值為value。ThreadLocalMap是一個內(nèi)部類,它是專門用于ThreadLocal的,與一般的Map不同,它的鍵類型為WeakReference<ThreadLocal>,我們沒有提過WeakReference,它與Java的垃圾回收機制有關(guān),使用它,便于回收內(nèi)存,具體我們就不探討了。

get方法的代碼為:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null)            return (T)e.value;
    }    return setInitialValue();
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

通過線程訪問到Map,以ThreadLocal對象為鍵從Map中獲取到條目,取其value,如果Map中沒有,調(diào)用setInitialValue,其代碼為:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xù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;
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

initialValue()就是之前提到的提供初始值的方法,默認實現(xiàn)就是返回null。

remove方法的代碼也很直接,如下所示:

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

簡單總結(jié)下,每個線程都有一個Map,對于每個ThreadLocal對象,調(diào)用其get/set實際上就是以ThreadLocal對象為鍵讀寫當(dāng)前線程的Map,這樣,就實現(xiàn)了每個線程都有自己的獨立拷貝的效果。

線程池與ThreadLocal

我們在78節(jié)介紹過線程池,我們知道,線程池中的線程是會重用的,如果異步任務(wù)使用了ThreadLocal,會出現(xiàn)什么情況呢?可能是意想不到的,我們看個簡單的示例:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

public class ThreadPoolProblem {    static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() {

        @Override        protected AtomicInteger initialValue() {            return new AtomicInteger(0);
        }
    };    static class Task implements Runnable {

        @Override        public void run() {
            AtomicInteger s = sequencer.get();            int initial = s.getAndIncrement();            // 期望初始為0            System.out.println(initial);
        }
    }    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new Task());
        executor.execute(new Task());
        executor.execute(new Task());
        executor.shutdown();
    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

對于異步任務(wù)Task而言,它期望的初始值應(yīng)該總是0,但運行程序,結(jié)果卻為:

0
0
1

第三次執(zhí)行異步任務(wù),結(jié)果就不對了,為什么呢?因為線程池中的線程在執(zhí)行完一個任務(wù),執(zhí)行下一個任務(wù)時,其中的ThreadLocal對象并不會被清空,修改后的值帶到了下一個異步任務(wù)。那怎么辦呢?有幾種思路:

  1. 第一次使用ThreadLocal對象時,總是先調(diào)用set設(shè)置初始值,或者如果ThreaLocal重寫了initialValue方法,先調(diào)用remove

  2. 使用完ThreadLocal對象后,總是調(diào)用其remove方法

  3. 使用自定義的線程池

我們分別來看下,對于第一種,在Task的run方法開始處,添加set或remove代碼,如下所示:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

static class Task implements Runnable {

    @Override    public void run() {
        sequencer.set(new AtomicInteger(0));        //或者 sequencer.remove();        
        AtomicInteger s = sequencer.get();        //...    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

對于第二種,將Task的run方法包裹在try/finally中,并在finally語句中調(diào)用remove,如下所示:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

static class Task implements Runnable {

    @Override    public void run() {        try{
            AtomicInteger s = sequencer.get();            int initial = s.getAndIncrement();            // 期望初始為0            System.out.println(initial);    
        }finally{
            sequencer.remove();
        }
    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

以上兩種方法都比較麻煩,需要更改所有異步任務(wù)的代碼,另一種方法是擴展線程池ThreadPoolExecutor,它有一個可以擴展的方法:

protected void beforeExecute(Thread t, Runnable r) { }

在線程池將任務(wù)r交給線程t執(zhí)行之前,會在線程t中先執(zhí)行beforeExecure,可以在這個方法中重新初始化ThreadLocal。如果知道所有需要初始化的ThreadLocal變量,可以顯式初始化,如果不知道,也可以通過反射,重置所有ThreadLocal,反射的細節(jié)我們會在后續(xù)章節(jié)進一步介紹。

我們創(chuàng)建一個自定義的線程池MyThreadPool,示例代碼如下:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

static class MyThreadPool extends ThreadPoolExecutor {    public MyThreadPool(int corePoolSize, int maximumPoolSize,            long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue) {        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override    protected void beforeExecute(Thread t, Runnable r) {        try {            //使用反射清空所有ThreadLocal
            Field f = t.getClass().getDeclaredField("threadLocals");
            f.setAccessible(true);
            f.set(t, null);
        } catch (Exception e) {
            e.printStackTrace();
        }        super.beforeExecute(t, r);
    }
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

這里,使用反射,找到線程中存儲ThreadLocal對象的Map變量threadLocals,重置為null。使用MyThreadPool的示例代碼如下:

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

public static void main(String[] args) {
    ExecutorService executor = new MyThreadPool(2, 2, 0,
            TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>());
    executor.execute(new Task());
    executor.execute(new Task());
    executor.execute(new Task());
    executor.shutdown();
}

電腦培訓(xùn),計算機培訓(xùn),平面設(shè)計培訓(xùn),網(wǎng)頁設(shè)計培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

使用以上介紹的任意一種解決方案,結(jié)果就符合期望了。

小結(jié)

本節(jié)介紹了ThreadLocal的基本概念、用法用途、實現(xiàn)原理、以及和線程池結(jié)合使用時的注意事項,簡單總結(jié)來說:

  • ThreadLocal使得每個線程對同一個變量有自己的獨立拷貝,是實現(xiàn)線程安全、減少競爭的一種方案。

  • ThreadLocal經(jīng)常用于存儲上下文信息,避免在不同代碼間來回傳遞,簡化代碼。

  • 每個線程都有一個Map,調(diào)用ThreadLocal對象的get/set實際就是以ThreadLocal對象為鍵讀寫當(dāng)前線程的該Map。

  • 在線程池中使用ThreadLocal,需要注意,確保初始值是符合期望的。

65節(jié)到現(xiàn)在,我們一直在探討并發(fā),至此,基本就結(jié)束了,下一節(jié),讓我們一起簡要回顧總結(jié)一下。

(與其他章節(jié)一樣,本節(jié)所有代碼位于 https://github.com/swiftma/program-logic,另外,與之前章節(jié)一樣,本節(jié)代碼基于Java 7, Java 8有些變動,我們會在后續(xù)章節(jié)統(tǒng)一介紹Java 8的更新)

----------------

未完待續(xù),查看最新文章,敬請關(guān)注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術(shù)的本質(zhì)。用心原創(chuàng),保留所有版權(quán)。

http://www.cnblogs.com/swiftma/p/6764821.html