【轉載】計算機程序的思惟邏輯 (82) - 理解ThreadLocal

本節,咱們來探討一個特殊的概念,線程本地變量,在Java中的實現是類ThreadLocal,它是什麼?有什麼用?實現原理是什麼?讓咱們接下來逐步探討。git

基本概念和用法github

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

ThreadLocal是一個泛型類,接受一個類型參數T,它只有一個空的構造方法,有兩個主要的public方法:swift

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

set就是設置值,get就是獲取值,若是沒有值,返回null,看上去,ThreadLocal就是一個單一對象的容器,好比:安全

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

輸出爲100。服務器

那ThreadLocal有什麼特殊的呢?特殊發生在有多個線程的時候,看個例子:併發

複製代碼
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());
    }
}
複製代碼

local是一個靜態變量,main方法建立了一個子線程child,main和child都訪問了local,程序的輸出爲:框架

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

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

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

protected T initialValue()
public void remove()

initialValue用於提供初始值,它是一個受保護方法,能夠經過匿名內部類的方式提供,當調用get方法時,若是以前沒有設置過,會調用該方法獲取初始值,默認實現是返回null。remove刪掉當前線程對應的值,若是刪掉後,再次調用get,會再調用initialValue獲取初始值。看個簡單的例子:

複製代碼
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());
    }
}
複製代碼

輸出值都是100。

使用場景

ThreadLocal有什麼用呢?咱們來看幾個例子。

DateFormat/SimpleDateFormat

ThreadLocal是實現線程安全的一種方案,好比對於DateFormat/SimpleDateFormat,咱們在32節介紹過日期和時間操做,提到它們是非線程安全的,實現安全的一種方式是使用鎖,另外一種方式是每次都建立一個新的對象,更好的方式就是使用ThreadLocal,每一個線程使用本身的DateFormat,就不存在安全問題了,在線程的整個使用過程當中,只須要建立一次,又避免了頻繁建立的開銷,示例代碼以下:

複製代碼
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);
    }
}
複製代碼

須要說明的是,ThreadLocal對象通常都定義爲static,以便於引用。

ThreadLocalRandom

即便對象是線程安全的,使用ThreadLocal也能夠減小競爭,好比,咱們在34節介紹過Random類,Random是線程安全的,但若是併發訪問競爭激烈的話,性能會降低,因此Java併發包提供了類ThreadLocalRandom,它是Random的子類,利用了ThreadLocal,它沒有public的構造方法,經過靜態方法current獲取對象,好比:

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

current方法的實現爲:

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

localRandom就是一個ThreadLocal變量:

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

上下文信息

ThreadLocal的典型用途是提供上下文信息,好比在一個Web服務器中,一個線程執行用戶的請求,在執行過程當中,不少代碼都會訪問一些共同的信息,好比請求信息、用戶身份信息、數據庫鏈接、當前事務等,它們是線程執行過程當中的全局信息,若是做爲參數在不一樣代碼間傳遞,代碼會很囉嗦,這時,使用ThreadLocal就很方便,因此它被用於各類框架如Spring中,咱們看個簡單的示例:

複製代碼
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);
    }
}
複製代碼

在首次獲取到信息時,調用set方法如setCurrentRequest/setCurrentUserId進行設置,而後就能夠在代碼的任意其餘地方調用get相關方法進行獲取了。

基本實現原理

ThreadLocal是怎麼實現的呢?爲何對同一個對象的get/set,每一個線程都能有本身獨立的值呢?咱們直接來看代碼。

set方法的代碼爲:

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

它調用了getMap,getMap的代碼爲:

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

返回線程的實例變量threadLocals,它的初始值爲null,在null時,set調用createMap初始化,代碼爲:

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

從以上代碼能夠看出,每一個線程都有一個Map,類型爲ThreadLocalMap,調用set其實是在線程本身的Map裏設置了一個條目,鍵爲當前的ThreadLocal對象,值爲value。ThreadLocalMap是一個內部類,它是專門用於ThreadLocal的,與通常的Map不一樣,它的鍵類型爲WeakReference<ThreadLocal>,咱們沒有提過WeakReference,它與Java的垃圾回收機制有關,使用它,便於回收內存,具體咱們就不探討了。

get方法的代碼爲:

複製代碼
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();
}
複製代碼

經過線程訪問到Map,以ThreadLocal對象爲鍵從Map中獲取到條目,取其value,若是Map中沒有,調用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;
}
複製代碼

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

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

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

簡單總結下,每一個線程都有一個Map,對於每一個ThreadLocal對象,調用其get/set實際上就是以ThreadLocal對象爲鍵讀寫當前線程的Map,這樣,就實現了每一個線程都有本身的獨立拷貝的效果。

線程池與ThreadLocal

咱們在78節介紹過線程池,咱們知道,線程池中的線程是會重用的,若是異步任務使用了ThreadLocal,會出現什麼狀況呢?多是意想不到的,咱們看個簡單的示例:

複製代碼
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();
    }
}
複製代碼

對於異步任務Task而言,它指望的初始值應該老是0,但運行程序,結果卻爲:

0
0
1

第三次執行異步任務,結果就不對了,爲何呢?由於線程池中的線程在執行完一個任務,執行下一個任務時,其中的ThreadLocal對象並不會被清空,修改後的值帶到了下一個異步任務。那怎麼辦呢?有幾種思路:

  1. 第一次使用ThreadLocal對象時,老是先調用set設置初始值,或者若是ThreaLocal重寫了initialValue方法,先調用remove
  2. 使用完ThreadLocal對象後,老是調用其remove方法
  3. 使用自定義的線程池

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

複製代碼
static class Task implements Runnable {

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

對於第二種,將Task的run方法包裹在try/finally中,並在finally語句中調用remove,以下所示:

複製代碼
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();
        }
    }
}
複製代碼

以上兩種方法都比較麻煩,須要更改全部異步任務的代碼,另外一種方法是擴展線程池ThreadPoolExecutor,它有一個能夠擴展的方法:

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

在線程池將任務r交給線程t執行以前,會在線程t中先執行beforeExecure,能夠在這個方法中從新初始化ThreadLocal。若是知道全部須要初始化的ThreadLocal變量,能夠顯式初始化,若是不知道,也能夠經過反射,重置全部ThreadLocal,反射的細節咱們會在後續章節進一步介紹。

咱們建立一個自定義的線程池MyThreadPool,示例代碼以下:

複製代碼
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);
    }
}
複製代碼

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

複製代碼
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();
}
複製代碼

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

小結

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

  • ThreadLocal使得每一個線程對同一個變量有本身的獨立拷貝,是實現線程安全、減小競爭的一種方案。
  • ThreadLocal常常用於存儲上下文信息,避免在不一樣代碼間來回傳遞,簡化代碼。
  • 每一個線程都有一個Map,調用ThreadLocal對象的get/set實際就是以ThreadLocal對象爲鍵讀寫當前線程的該Map。
  • 在線程池中使用ThreadLocal,須要注意,確保初始值是符合指望的。

65節到如今,咱們一直在探討併發,至此,基本就結束了,下一節,讓咱們一塊兒簡要回顧總結一下。

(與其餘章節同樣,本節全部代碼位於 https://github.com/swiftma/program-logic,另外,與以前章節同樣,本節代碼基於Java 7, Java 8有些變更,咱們會在後續章節統一介紹Java 8的更新)

相關文章
相關標籤/搜索