Java併發學習之ThreadLocal使用及原理介紹

ThreadLocal使用及原理介紹

線程本地變量,每一個線程保存變量的副本,對副本的改動,對其餘的線程而言是透明的(即隔離的)java

1. 使用姿式一覽

使用方式也比較簡單,經常使用的三個方法web

// 設置當前線程的線程局部變量的值
void set(Object value); 

// 該方法返回當前線程所對應的線程局部變量
public Object get();

// 將當前線程局部變量的值刪除
public void remove();

下面給個實例,來瞅一下,這個東西通常的使用姿式。一般要獲取線程變量, 直接調用 ParamsHolder.get()算法

public class ParamsHolder {
    private static final ThreadLocal<Params> PARAMS_INFO = new ThreadLocal<>();

    @ToString
    @Getter
    @Setter
    public static class Params {
        private String mk;
    }

    public static void setParams(Params params) {
        PARAMS_INFO.set(params);
    }

    public static void clear() {
        PARAMS_INFO.remove();
    }
    
    public static Params get() {
        return PARAMS_INFO.get();
    }
    
    
    public static void main(String[] args) {
        Thread child = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child thread initial: " + ParamsHolder.get());
                ParamsHolder.setParams(new ParamsHolder.Params("thread"));
                System.out.println("child thread final: " + ParamsHolder.get());
            }
        });


        child.start();

        System.out.println("main thread initial: " + ParamsHolder.get());
        ParamsHolder.setParams(new ParamsHolder.Params("main"));
        System.out.println("main thread final: " + ParamsHolder.get());
    }
}

輸出結果數組

child thread initial: null
main thread initial: null
child thread final: ParamsHolder.Params(mk=thread)
main thread final: ParamsHolder.Params(mk=main)

2. 實現原理探究

直接看源碼中的兩個方法, get/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);
}

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

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

主要以set方法進行講解

邏輯比較清晰多線程

  • 獲取當前線程對象
  • 獲取到線程對象中的threadLocals 屬性
  • 將value塞入ThreadLocalMap

threadLocals屬性ide

這個屬性的解釋以下,簡單來說,這個裏面的變量都是線程獨享的,徹底由線程本身hold住學習

ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class.this

接下來須要瞭解的就是ThreadLocalMap這個對象的內部構造了,裏面的有個table對象,維護了一個Entry的數組tableEntry的key爲ThreadLocal對象,value爲具體的值。線程

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

       Entry(ThreadLocal<?> k, Object v) {
           super(k);
           value = v;
       }
   }
   
   /**
    * The table, resized as necessary.
    * table.length MUST always be a power of two.
    */
private Entry[] table;
   
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    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;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

聚焦在 int i = key.threadLocalHashCode & (table.length - 1); 這一行,這個就是獲取Entry對象在table中索引值的主要邏輯,主要利用當前線程的hashCode值

假設出現兩個不一樣的線程,這個code值同樣,會如何?

這種相似hash碰撞的場景,會調用 nextIndex 來獲取下一個位置


針對上面的邏輯,有點有必要繼續研究下, hashCode 的計算方式, 爲何要和數組的長度進行與計算

做爲ThreadLocal實例的變量只有 threadLocalHashCode 這一個,nextHashCodeHASH_INCREMENT 是ThreadLocal類的靜態變量,實際上HASH_INCREMENT是一個常量,表示了連續分配的兩個ThreadLocal實例的threadLocalHashCode值的增量,而nextHashCode 的表示了即將分配的下一個ThreadLocal實例的threadLocalHashCode 的值

全部ThreadLocal對象共享一個AtomicInteger對象nextHashCode用於計算hashcode,一個新對象產生時它的hashcode就肯定了,算法是從0開始,以HASH_INCREMENT = 0x61c88647爲間隔遞增,這是ThreadLocal惟一須要同步的地方。根據hashcode定位桶的算法是將其與數組長度-1進行與操做

ThreadLocalMap的初始長度爲16,每次擴容都增加爲原來的2倍,即它的長度始終是2的n次方,上述算法中使用0x61c88647可讓hash的結果在2的n次方內儘量均勻分佈,減小衝突的機率

3. 線程池中使用ThreadLocal的注意事項

這裏主要的一個問題是線程複用時, 若是不清除掉ThreadLocal 中的值,就會有可怕的事情發生, 先簡單的演示一下

private static final ThreadLocal<AtomicInteger> threadLocal =new ThreadLocal<AtomicInteger>() {

        @Override
        protected AtomicInteger initialValue() {
            return new AtomicInteger(0);
        }
    };


    static class Task implements Runnable {

        @Override
        public void run() {
            AtomicInteger s = threadLocal.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();
    }

輸出結果

0
0
1

說好的線程變量,這裏竟然沒有按照咱們預期的來玩,主要緣由就是線程複用了,而線程中的局部變量沒有清零,致使下一個使用這個線程的時候,這些局部變量也帶過來,致使沒有按照咱們的預期使用

這個最可能致使的一個超級嚴重的問題,就是web應用中的用戶串掉的問題,若是咱們將每一個用戶的信息保存在 ThreadLocal 中, 若是出現線程複用了,那麼問題就會致使明明是張三用戶,結果登陸顯示的是李四的賬號,這下就真的呵呵了

所以,強烈推薦,對於線程變量,一但不用了,就顯示的調用 remove()方法進行清楚

4. 經典case

SimpleDataFormate 是一個非線程安全的類,可使用 ThreadLocal 完成的線程安全的使用

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

想想,爲何這種方式是線程安全的呢?

II. 小結

1. 一句話介紹

ThreadLocal 線程本地變量,每一個線程保存變量的副本,對副本的改動,對其餘的線程而言是透明的(即隔離的)

2. 經常使用方法

三個經常使用的方法

// 設置當前線程的線程局部變量的值
void set(Object value); 

// 該方法返回當前線程所對應的線程局部變量
public Object get();

// 將當前線程局部變量的值刪除
public void remove();

3. 實現原理

利用了HashMap的設計理念,一個map中存儲Thread->線程變量的映射關係, 所以線程變量在多線程之間是隔離的

4. 注意事項

一般建議是線程執行完畢以後,主動去失效掉ThreadLocal中的變量,以防止線程複用致使變量被亂用了

III. 其餘

聲明

盡信書則不如,已上內容,純屬一家之言,因本人能力通常,見識有限,若有問題,請不吝指正,感激

掃描關注,不定時分享各類java學習筆記

QrCode

相關文章
相關標籤/搜索