Java併發程式設計:ThreadLocal的使用以及實現原理解析

xue無止境發表於2019-01-03

前言

前面的文章裡,我們學習了有關鎖的使用,鎖的機制是保證同一時刻只能有一個執行緒訪問臨界區的資源,也就是通過控制資源的手段來保證執行緒安全,這固然是一種有效的手段,但程式的執行效率也因此大大降低。那麼,有沒有更好的方式呢?答案是有的,既然鎖是嚴格控制資源的方式來保證執行緒安全,那我們可以反其道而行之,增加更多資源,保證每個執行緒都能得到所需物件,各自為營,互不影響,從而達到執行緒安全的目的,而ThreadLocal便是採用這樣的思路。

ThreadLocal例項

ThreadLocal翻譯成中文的話大概可以說是:執行緒區域性變數,也就是隻有當前執行緒能夠訪問。它的設計作用是為每一個使用該變數的執行緒都提供一個變數值的副本,每個執行緒都是改變自己的副本並且不會和其他執行緒的副本衝突,這樣一來,從執行緒的角度來看,就好像每個執行緒都擁有了該變數。

下面是一個簡單的例項:

public class ThreadLocalDemo {

    static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static class MyRunnable implements Runnable{

        @Override
        public void run() {
            for (int i = 0;i<3;i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int value = local.get();
                System.out.println(Thread.currentThread().getName() + ":" + value);
                local.set(value + 1);
            }
        }
    }

    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

上面的程式碼不難理解,首先是定義了一個名為 local 的ThreadLocal變數,並初識變數的值為0,然後是定義了一個實現Runnable介面的內部類,在其run方法中對local 的值做讀取和加1的操作,最後是main方法中開啟兩個執行緒來執行內部類例項。

以上就是程式碼的大概邏輯,執行main函式後,程式的輸出結果如下:

Thread-0:0
Thread-1:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-0:2

從結果可以看出,雖然兩個執行緒都共用一個Runnable例項,但兩個執行緒中所展示的ThreadLocal的資料值並不會相互影響,也就是說這種情況下的local 變數儲存的資料相當於是執行緒安全的,只能被當前執行緒訪問。

ThreadLocal實現原理

那麼ThreadLocal內部是怎麼保證物件是執行緒私有的呢?毫無疑問,答案需要從原始碼中查詢。回顧前面的程式碼,可以發現其中呼叫了ThreadLocal的兩個方法setget,我們就從這兩個方法入手。

先看 set() 的原始碼:

public void set(T value) {
    Thread t = Thread.currentThread();
    // 獲取執行緒的ThreadLocalMap,返回map
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        //map為空,建立
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set的程式碼邏輯比較簡單,主要是把值設定到當前執行緒的一個ThreadLocalMap物件中,而ThreadLocalMap可以理解成一個Map,它是定義在Thread類中內部的成員,初始化是為null,

ThreadLocal.ThreadLocalMap threadLocals = null;

不過,與常見的Map實現類,如HashMap之類的不同的是,ThreadLocalMap中的Entry是繼承於WeakReference類的,保持了對 “鍵” 的弱引用和對 “值” 的強引用,這是類的原始碼:

static class ThreadLocalMap {

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

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    //省略剩下的原始碼
    ....................
}

從原始碼中中可以看出,Entry建構函式中的引數 k 就是ThreadLocal例項,呼叫super(k) 表明對 k 是弱引用,使用弱引用的原因在於,當沒有強引用指向 ThreadLocal 例項時,它可被回收,從而避免記憶體洩露,那麼為何需要防止記憶體洩露呢?原因下面會說到。

接著說set方法的邏輯,當呼叫set方法時,其實是將資料寫入threadLocals這個Map物件中,這個Map的key為ThreadLocal當前物件,value就是我們存入的值。而threadLocals本身能儲存多個ThreadLocal物件,相當於一個ThreadLocal集合。

接著看 get() 的原始碼:

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;
        }
    }
    //設定初識值到ThreadLocal中並返回
    return 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;
    }

get方法的邏輯也是比較簡單的,就是直接獲取當前執行緒的ThreadLocalMap物件,如果該物件不為空就返回它的value值,否則就把初始值設定到ThreadLocal中並返回。

看到這,我們大概就能明白為什麼ThreadLocal能實現執行緒私有的原理了,其實就是每個執行緒都維護著一個ThreadLocal的容器,這個容器就是ThreadLocalMap,可以儲存多個ThreadLocal物件。而呼叫ThreadLocal的set或get方法其實就是對當前執行緒的ThreadLocal變數操作,與其他執行緒是分開的,所以才能保證執行緒私有,也就不存線上程安全的問題了。

然而,該方案雖然能保證執行緒私有,但卻會佔用大量的記憶體,因為每個執行緒都維護著一個Map,當訪問某個ThreadLocal變數後,執行緒會在自己的Map內維護該ThreadLocal變數與具體實現的對映,如果這些對映一直存在,就表明ThreadLocal 存在引用的情況,那麼系統GC就無法回收這些變數,可能會造成記憶體洩露。

針對這種情況,上面所說的ThreadLocalMap中Entry的弱引用就起作用了。

TheadLocal與同步機制的區別

最後,總結一下ThreadLocal和同步機制之間的區別吧。

實現機制:

同步機制採用了“以時間換空間”的方式,控制資源保證同一時刻只能有一個執行緒訪問。

ThreadLocal採用了“以空間換時間”的方式,為每一個執行緒都提供一份變數的副本,從而實現同時訪問而互不影響,但因為每個執行緒都維護著一份副本,對記憶體空間的佔用會增加。

資料共享:

同步機制是對公共資源做控制訪問的方式來保證執行緒安全,但資源仍是共享狀態,可用於執行緒間的通訊;

ThreadLocal是每個執行緒都有自己的資源(變數)副本,互相之間不影響,也就不存在共享的說法了。

相關文章