ThreadLocal詳解

前言

ThreadLocal工作中會偶有用到,是解決thread間的數據隔離問題的(並不是爲解決併發和共享問題的),也是面試常見問題,比如:ThreadLocal知道嗎?說說你自己的理解?或者這樣問:在多線程環境下,如何防止自己的變量被其它線程篡改?無論基於哪種原因都是很有必要學習的。

ThreadLocal是什麼

貼一段源碼中的介紹:這個類提供線程的局部變量,可以通過get()和set()方法來獲取和設置自己的局部變量;ThreadLocal實例通常是pricate static fields的,希望將信息關聯到一個線程中,例如:user ID、Transaction ID

概括的說:存儲各個線程互不相同的信息,實現線程間的數據隔離

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

ThreadLocal能做什麼

  1. 實例1:參數傳遞(用戶信息傳遞)

    當前用戶信息需要被線程內的所有方法共享

    • 方案1:傳遞參數

      將user作爲參數在每個方法中進行傳遞,缺點:會產生代碼冗餘問題,並且可維護性差

    • 方案2:使用Map

      缺點:如果在單線程環境下可以保證安全,但是在多線程環境下是不可以的。如果使用加鎖或是ConcurrentHashMap都會產生性能問題。

    • 方案3:使用ThreadLocal,實現不同方法間的資源共享

public class ThreadLocalNormalUsage02 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("張三");
        //將User對象存儲到 holder 中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service2拿到用戶名: " + user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("Service3拿到用戶名: " + user.name);
    }
}


class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

結果:

總結:實際項目中也會用到這個實例,在請求的攔截器中添加用戶信息到ThreadLocal< User >的ThreadLocalMap中,這樣同一線程中的各個方法或組建就可以獲取到對應的用戶信息。

  1. 實例2:典型工具類(SimpleDateFormat和Random)
class ThreadSafeFormatter {
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        //創建一份 SimpleDateFormat 對象
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

ThreadLocal爲每個線程創建獨立的SimpleDateFormat對象(實際應用中,針對不通的format形式,會創建不同的方法,方法中會是一個新SimpleDateFormat對象),當然也可以使用LocalDateTime。
注:可參考這篇博客——地址

ThreadLocal原理

  • 首先看set()方法:
public void set(T value) {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //獲取ThreadLocalMap,這個map是什麼那?下面有介紹
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //如果map存在,則以當前線程t爲key,數據爲value放到map
        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);
}

再來看一下ThreadLocalMap

static class ThreadLocalMap {
/**
 * 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;
    }
    //map的其他定義
}

ThreadLocalMap實際上是ThreadLocal的一個靜態內部類,數據就存放在Entry中;

那這個map如何從Thread中獲取的那?看下面getMap()和Thread類中的代碼

//ThreadLocal中的getMap方法
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * Thread的threadLocals
 */
ThreadLocal.ThreadLocalMap threadLocals = null;

實際上Thread類中維護了一個ThreadLocalMap變量。

如果map不存在,則創建一個,源碼如下:

/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  • 大致瞭解set的過程,再來看get過程就比較容易理解了
/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    //獲取線程對象t
    Thread t = Thread.currentThread();
    //在對象t中獲取ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //從Entry中獲取數據
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

如果沒有獲取map(可能沒有set值),則初始化value值

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

protected T initialValue() {
    return null;
}

實際上value是null。

  • 刪除數據:
private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}
  • 總結:ThreadLocal實現數據隔離主要是依賴ThreadLocalMap,而每一個線程中都有一個ThreadLocal.ThreadLocalMap變量,這樣在set和get數據時,都是獲取對應線程中的ThreadLocalMap,數據則存在map裏面的Entry中,key爲Threalocal對象。

關於ThreadLocal內存泄漏

  • 內存泄漏的原因:

    • 個人認爲ThreadLocal內存泄漏只是有可能,是個小概率事件
    • 內存泄露:某個對象不會再被使用,但是該對象的內存卻無法被收回
     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;
             }
     }
    
    • 強引用:當內存不足時觸發GC,寧願拋出OOM也不會回收強引用的內存

    • 弱引用:觸發GC後便會回收弱引用的內存

    • 分析:

      • 正常情況:當Thread運行結束後,ThreadLocal中的value會被回收,因爲沒有任何強引用了
      • 非正常情況:當Thread一直在運行始終不結束,強引用就不會被回收,存在以下調用鏈——Thread–>ThreadLocalMap–>Entry(key爲null)–>value;因爲調用鏈中的 value 和 Thread,存在強引用,所以value無法被回收,就有可能出現OOM。

      JDK的設計已經考慮到了這個問題,所以在set()、remove()、resize()方法中會掃描到key爲null的Entry,並且把對應的value設置爲null,這樣value對象就可以被回收。

  • 如何避免內存泄漏:

    調用remove()方法,就會刪除對應的Entry對象,可以避免內存泄漏,所以使用完ThreadLocal後,要調用remove()方法。

NPE問題

public class ThreadLocalNPE {
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
    }

    public Long get() {
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        //如果get方法返回值爲基本類型,則會報空指針異常,如果是包裝類型就不會出錯
        System.out.println("out:" + threadLocalNPE.get());

        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println("inner:" + threadLocalNPE.get());
            }
        }).start();
    }
}

上面例子出現NPE問題,主要是get方法返回值類型,我們知道在未對ThreadLocalMap進行set值時,如果直接獲取,代碼會給我們初始化一個null保存到map中;如果上面get方法返回值是基本數據類型,則會出現裝箱和拆箱,導致NPE。

共享數據問題

如果在每個線程中ThreadLocal.set()進去的東西本來就是多個線程共享的同一對象,比如static對象,那麼多個線程調用ThreadLocal.get()獲取的內容還是同一個對象,還是會發生線程安全問題

總結

可以不使用ThreadLocal就不要強行使用:如果在任務數很少的時候,在局部方法中創建對象就可以解決問題,這樣就不需要使用ThreadLocal。

優先使用框架的支持,而不是自己創造:例如在Spring框架中,如果可以使用RequestContextHolder,那麼就不需要自己維護ThreadLocal,因爲自己可能會忘記調用remove()方法等,造成內存泄漏。

參考博文:

https://juejin.im/post/5e0d8765f265da5d332cde44#heading-5
https://juejin.im/post/5ac2eb52518825555e5e06ee#heading-4
https://juejin.im/post/5e0d8765f265da5d332cde44#heading-13
https://www.jianshu.com/p/6bf1adb775e0
https://www.jianshu.com/p/98b68c97df9b
https://blog.csdn.net/lufeng20/article/details/24314381?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
https://www.jianshu.com/p/377bb840802f

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