Java高併發--ThreadLocal使用及源碼分析

1概述

線程本地變量(ThreadLocal)位每一個線程創建一個變量副本,從而線程就可以單獨使用自己擁有的變量副本,而相互之間不產生影響。而線程本地變量與線程同步機制中的共享變量有什麼區別呢?很明顯,每個線程對共享變量的修改對於其餘線程是可見的,而每個線程對線程本地變量的修改對於其餘線程是不可見的。

2示例

package com.liutao.concurrent;

public class ThreadLocalDemo {
    public static ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        int num;
        for (int i = 0; i < 3;i++){
            num = ThreadLocalDemo.integerThreadLocal.get();
            System.out.println(Thread.currentThread().getName()+":"+num);
            ThreadLocalDemo.integerThreadLocal.set(num+1);
        }
    }
}

執行結果:

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

我們可以看見上面三個線程執行的內容是相同的,相互之間沒有任何影響。

針對ThreadLocal常用的四個方法如下:

  • get:返回當前線程中變量副本的值。
  • set:設置當前線程中變量副本的值。
  • remove:移除當前線程中變量副本的值。
  • initialValue:返回變量副本的初始值。當線程第一次調用變量副本的時候將會調用這個方法,除非在調用變量副本的get方法之前已經調用了set方法進行設置。當然上面的示例我們可以看見,我們重寫了initialValue方法。

3源碼分析

3.1ThreadLocalMap源碼分析

通過查看ThreadLocal的set、get方法我們可以發現ThreadLocal的實現主要是依靠了ThreadLocalMap這個內部類。那麼首先我們來看ThreadLocalMap這個內部類的實現。

通過查看源碼我們可以發現ThreadLocalMap是使用Entry來進行key和value的存儲的。

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** 和線程產生關聯的值 */
            Object value;

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

這個Entry繼承自WeakReference,並使用ThreadLocal作爲key。可以看見針對弱引用,當key爲null的時候將被GC回收。

那麼針對ThreadLocalMap我們僅僅分析兩個核心方法即可。

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be 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[] tab = table;
            int len = tab.length;

            //根據ThreadLocal的hashcode查找對應元素在數組中的位置
            int i = key.threadLocalHashCode & (len-1);
            

            //尋找合適的位置(位置上有值)
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                
                //如果已經存在,則覆蓋原值
                if (k == key) {
                    e.value = value;
                    return;
                }
  
                //key == null,但是存在值(因爲此處的e != null),說明之前的ThreadLocal對象已經被回收了
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            
            //ThreadLocal對應的key不存在,也沒有Entry值,則創建一個新的
            tab[i] = new Entry(key, value);
            int sz = ++size;

           //cleanSomeSlots清楚key == null 的Entry,如果沒有清除成功,並且數組中的元素大於了閥值則refresh
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

針對上面的源碼,我們可以看見ThreadLocalMap初始的時候有一個table,並且初始容量爲16。

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

nextIndex方法的源碼如下:

 private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

可以發現輪詢table獲取的Entry的最大索引爲len-1,當大於len-1的時候索引爲0。

針對cleanSomeSlots和refresh,我們可以看見,最終都是調用expungeStableEntry來刪除Entry的value並置空節點。

/**
 * 清除成舊的Entry(key==null)
 */
private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }


 private void rehash() {
            expungeStaleEntries();

            
            if (size >= threshold - threshold / 4)
                resize();
        }

/**
 * 清除整個table的成舊數據
 */
private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }
/**
 * 從新hash計算從staleSlot節點到下一個空節點中的節點
 */
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            //刪除指定節點位置的值並置空節點
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            //從新計算hash,直到遇到null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                //刪除節點值並置空節點
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

通過上面我們分析了ThreadLocalMap的set方法,接下來我們分析getEntry,源碼如下:

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

通過key計算得到索引值過後直接從table中獲取值,如果獲取到就返回,沒有獲取到就調用getEntryAfterMiss,那麼getEntryAfterMiss又做了什麼呢?

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

從i位置開始查找entry,當找到 entry不爲空,並且找到對應的key的時候,就返回entry,如果key ==null就直接清除掉位置i。

前面分析了ThreadLocalMap中的核心方法,那麼針對ThreadLocal中的方法相對來說就簡單多了。

3.2ThreadLocal源碼分析

(1)get方法

public T get() {
        Thread t = Thread.currentThread();
        
        //獲取當前線程的ThreadLocalMap
        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();
    }

返回當前線程中變量副本的值,如果沒有 ,就初始化。

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

設置當前線程的變量副本的值爲value,當然如果ThreadLocalMap不存在,則先創建ThreadLocalMap,然後再進行初始值設置。

針對remove和initialValue就不繼續講解了,都比較簡單。

4應用場景

最常見的ThreadLocal的使用時用來解決數據庫連接和session管理。

具體示例,後期在源碼中發現添加。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章