搞懂ThreadLocal

什麼是ThreadLocal

Java doc的解釋
ThreadLocal提供了線程局部可用的變量. 這些變量在每個線程中都維護有自己的獨立副本. ThreadLocal類型的變量一般是一個類的靜態變量來存儲與線程相關的變量 (比如用戶ID, 事務ID).

每個活的線程都持有thread-local變量的精確引用. 當線程terminated後, 該變量變得可以被GC(在沒有其他引用後).

ThreadLocal可以做什麼?

常見用途.

存儲一些本身不是線程安全的類

比如SimpleDateFormat不是線程安全的, 我們可以用ThreadLocal來存儲

import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalDemo {


    static ThreadLocal<SimpleDateFormat> _formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));


    public static String formate(Date date) {
        return _formatter.get().format(date);
    }

    public static void main(String[] args) {
        System.out.println(formate(new Date()));
    }
    

記錄一些上下文相關的信息

比如前面寫的userid, transactionid
我們用ThreadLocal來實現打印日誌中包含正確的Component信息.

import java.util.Date;

public class ThreadLocalLooggerDemo {


    public static void main(String[] args) throws InterruptedException {

        Thread task = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.setTaskId("Consumer");
                Log.info("I started");
                try {
                    Thread.sleep(1000);
                }
                catch (InterruptedException e) {
                }
                Log.info("I finished");
            }
        });
        task.start();
        task.join();
    }


    static class Log {
        static ThreadLocal<String> taskId = ThreadLocal.withInitial(() -> "N/A");
        public static void setTaskId(String taskIdStr) {
            taskId.set(taskIdStr);
        }

        public static String getTaskId() {
            return taskId.get();
        }


        public static void info(String message) {
            System.out.println(String.format("[%s][%s]%s", new Date(), taskId.get(), message));
        }
    }
}

示例輸出

[Wed Jul 10 13:59:10 CST 2019][Consumer]I started
[Wed Jul 10 13:59:11 CST 2019][Consumer]I finished

實現細節

set 方法

        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
  1. 每個Thread有一個自己的變量map: ThreadLocal.ThreadLocalMap threadLocals = null
  2. 該map是一個ThreadLocal內部定義的map. map裏面的每個Entry都是WeakReference<ThreadLocal>.
  3. 使用WeakReference是因爲 我們不能一直持有一個Thread或者ThreadLocal的引用這會導致它沒法被GC, 這就有了內存泄漏. 而WeakReference沒有這個問題. 這就是後面如果Entry的key爲NULL時, 它的值也被擦除了. (它的值就是這裏的SimpleDateFormat)
int i = key.threadLocalHashCode & (len-1); // 根據key (threadlocal對象)找到tab[]的index

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {  // 循環遍歷 tab,   nextIndex  就是從i開始自增 並取模len
                ThreadLocal<?> k = e.get();

                if (k == key) {  // 如果是同一個對象 指針比較
                    e.value = value;  // 更新值返回
                    return;
                }

                if (k == null) {  // 當前位置沒人用  就放在當前位置
                    replaceStaleEntry(key, value, i); // 該方法還會做一些動作 就是
                    return;
                }
            }

get方法

        Thread t = Thread.currentThread();  // 一直使用當前Thread的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();  // 沒有返回初始值 如果有設置的話
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e; // 直接根據hash值找
            else
               // 沒找到 或者不匹配
                return getEntryAfterMiss(key, i, e);
        }
        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);  // 擦除掉當前entry key爲null的值, key和value都爲NULL
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

// 擦除key爲NULL的方法

        private int expungeStaleEntry(int staleSlot) {  // 當前key爲null的slot
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot  // 擦除當前slot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--; 

            // Rehash until we encounter null
            Entry e;
            int i;
            // for 循環
            for (i = nextIndex(staleSlot, len); // 向下檢查下一個slot
                 (e = tab[i]) != null;  // Entry有東西
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {  // 擦除key 爲null的   
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        // 擦除更多
                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章