1. 程式人生 > >java ThreadLocal執行緒設定私有變數底層原始碼分析

java ThreadLocal執行緒設定私有變數底層原始碼分析

  前面也聽說了ThreadLocal來實現高併發,以前都是用鎖來實現,看了挺多資料的,發現其實還是區別挺大的(感覺嚴格來說ThreadLocal並不算高併發的解決方案),現在總結一下吧。

  高併發中會出現的問題就是執行緒安全問題,可以說是多個執行緒對共享資源訪問如何處理的問題,處理不當會的話,會出現結果和預期會完全不同。

  一般情況下,多個執行緒訪問一個變數都是公用他們的值,不過有時候雖然也是訪問共享變數,不過每個執行緒卻需要自己的私有變數。這個時候ThreadLocal就有用武之地了。下面是個ThreadLocal的簡單例項:

 

public class ThreadLocalExample {
    public static void main(String[] args){
        //建立一個ThreadLocal物件
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        
        //設定主執行緒私有變數值
        threadLocal.set(100);
        
        //建立一個新執行緒
        new Thread(new Runnable(){
            public void run(){
                //使用共享變數,設定執行緒私有變數
                threadLocal.set(50);
                System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
            }
        }).start();
        
        System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
    }
}

 

輸出結果:

main:100
Thread-0:50

  很神奇,對多個資源之間的共享,又不想他們之間相互影響,所以使用這個是挺不錯的。具體應用,spring中應用我記得挺多的,連線資料庫的每個連線,還有session的儲存。

  思考了一下,要我實現的話就用個map來儲存,因為這個其實就是鍵值對,只不過鍵是執行緒唯一標識,值就是對應的私有變數。

具體看了原始碼發現差不多,不過使用內部自己實現的一個ThreadLocalMap類,內部還一個Entry類而且Entry類繼承weakRefrence(說實話第一次遇到弱應用,以前只是在jvm那本書學習了下),具體方法如下:

  

先看下他的set方法吧

public void set(T value) {
    
        Thread t = Thread.currentThread();

        //獲得所有執行緒共享的ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);

        //物件已經存在就直接插入鍵值對
        //不存在就建立然後再插入
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

getMap方法的話一個獲得所有執行緒共享的ThreadLocalMap物件如下:

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

然後進入Thread類進去找一下這個容器,找到下面:

ThreadLocal.ThreadLocalMap threadLocals = null;

然後建立:

void createMap(Thread t, T firstValue) {
    //建立ThreadLocalMap物件賦給threadLocals t.threadLocals = new ThreadLocalMap(this, firstValue); }

至此,ThreadLocal的基本原理就已經很清晰了:各執行緒對共享的ThreadLocal例項進行操作,實際上是以該例項為鍵對內部持有的ThreadLocalMap物件進行操作。

還有get()方法的話就是利用設定的鍵進行獲取,remove()方法也是,其實和Hashmap差不多不過解決衝突使用的拉鍊法(對了,下次寫一篇HashHap的還有ConcurrentHashMap的話,頗有研究)。這裡有個問題就是因為這個ThreadLocalMap是靜態的所以在方法區中(jdk8之後為元資料區),不進行回收的話會造成記憶體洩漏,而且可能會出現記憶體溢位,所以使用後記得remove();

基本上其實可以了,不過好奇ThreadLocalMap怎麼實現的可以接著往下看,我也好奇,所以也偷偷看了,嘿嘿嘿

 

那就來分析一下這個ThreadLocalMap這個內部類吧。

ThreadLocalMap屬於一個自定義的map,是一個帶有hash功能的靜態內部類,其實和java.util包下提供的Map類並沒有關係。內部有一個靜態的Entry類,下面具體分析Entry。

/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

偷了一下官方的解釋:

主要是說entry繼承自WeakReference,用main方法引用的欄位作為entry中的key。當entry.get() == null的時候,意味著鍵將不再被引用。

注意看到一個super(k),說明呼叫父類的構造,去看看

Reference(T referent) {
    this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

  就上面這個其他沒了,看了半天有點沒看懂,然後去學了四種引用回來終於看懂,由於篇幅過多,在結尾我給出兩篇別人的部落格,可以去看完了,再回來,多學點哈哈哈。

再看了下發現這個內部類好多,但是其實就是map的一種實現,上面也講了set方法那就簡單提一下ThreadLocalMap的set()方法相關的吧,程式碼下面:

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

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            //把Entry物件陣列拿到來
            ThreadLocal.ThreadLocalMap.Entry[] tab = table;
            //長度也拿到來
            int len = tab.length;
            //通過拿到key的hashcode值,進去發現神奇的一幕這裡利用通過累加這個值0x61c88647來作為hashcode,
            // 這裡提一下往下走發現因為要公用這個屬性,多個例項訪問會有問題
            // 所以使用了AtomicInteger原子操作來寫值
            //並且與總長度-1做與運算就是取模,因為擴容都是2的n次方所以這樣直接取模就行,速度快
            int i = key.threadLocalHashCode & (len-1);

            //定位到對應的陣列位置,進行衝突判斷之類的處理
            for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {   //這裡是衝突遍歷

                //這裡裡面就拿對應tabel下對應位置的當前引用
                ThreadLocal<?> k = e.get();

                //判斷是不是對應的鍵,是的話就覆蓋
                if (k == key) {
                    e.value = value;
                    return;
                }
                //沒有的話就生成Entry代替掉
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //這裡就直接插入了
            tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
            //長度加1
            int sz = ++size;
            //判斷是否做擴容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

 

裡面其實挺複雜的,具體的話就是正常是使用開放定址法處理,這裡使用累加一個定值解決的衝突,因為多個例項共用,特殊處理,厲害厲害。

//threadLocalHashCode程式碼也貼在這裡吧,有興趣可以直接去看

        private static AtomicInteger nextHashCode = new AtomicInteger();
        private static final int HASH_INCREMENT = 0x61c88647;
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
        private final int threadLocalHashCode = nextHashCode();

 總結

看完原始碼之後神清氣爽,學到了很多啦。以前對java引用只是知道四個引用和對應的相應簡單概念,為了看懂這個Entry,去學習了weakReference原始碼,看了別人的關於四個引用的部落格寫的真好,偷偷學習了下,並且知道怎麼使用了。劃重點會用了!!!當然對於ThreadLocal也會用了,而且好像可以手寫一個簡單的版本哎,可以動手試試。

 

關於四種引用部落格,寫的真的很棒。

https://blog.csdn.net/swebin/article/details/78571933

https://blog.csdn.net/hacker_zhidian/article/details/8304