Android進階知識:ThreadLocal

一、ThreadLocal是什麼?

ThreadLocal是一個線程內部數據存儲類,經過他能夠在指定的線程中存儲數據。存儲後,只能在指定的線程中獲取到存儲的數據,對其餘線程來講沒法獲取到數據。java

二、ThreadLocal的使用場景

平常使用場景很少,當某些數據是以線程爲做用域而且不一樣線程具備不一樣的數據副本的時候,能夠考慮使用ThreadLocalAndroid源碼的LopperActivityThread以及AMS中都用到了ThreadLocalide

三、ThreadLocal的使用示例

public class ThreadLocalActivity extends AppCompatActivity {
private ThreadLocal<String> name = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_thread_local);
    name.set("小明");
    Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
    new Thread("thread1") {
        @Override
        public void run() {
            name.set("小紅");
            Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
        }
    }.start();
    new Thread("thread2") {
        @Override
        public void run() {
            Log.d("ThreadLocalActivity", "Thread:" + Thread.currentThread().getName() + " name:" + name.get());
        }
    }.start();
}
}
複製代碼

運行結果:this

D/ThreadLocalActivity: Thread:main name:小明  
D/ThreadLocalActivity: Thread:thread1 name:小紅  
D/ThreadLocalActivity: Thread:thread2 name:null
複製代碼

能夠看到雖然訪問的是同一個ThreadLocal對象,可是獲取到的值倒是不同的。spa

四、ThreadLocal的源碼閱讀

那麼爲何會形成這樣的結果呢?這就須要去看看ThreadLocal的源碼實現,這裏的源碼版本爲API28。主要看它的getset方法。
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);
}
複製代碼

set方法中首先獲取了當前線程對象,而後經過getMap方法傳入當前線程t獲取到一個ThreadLocalMap,接下來判斷這個map是否爲空,不爲空就直接將當前ThreadLocal做爲keyset方法中傳入要保存的值最爲value,存放到map中;若是map爲空就調用createMap方法建立一個map並一樣將當前ThreadLocal和要保存的值做爲keyvalue加入到map中。
接下先看getMap方法:設計

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
複製代碼

getMap方法比較簡單,就是返回從傳入的當前線程對象的成員變量threadLocals。 接着是createMap方法:code

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製代碼

createMap方法也很簡單就是new了一個ThreadLocalMap並賦給當前線程對象t中的threadLocals。 原來這個Map是存放在Thread類中的。因而進入Thread類中查看。
Thread.java第188-190行:對象

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
複製代碼

根據這裏的註釋能夠得知,每一個線程Thread中都有一個ThreadLocalMap類型的threadLocals成員變量來保存數據,經過ThreadLocal類來進行維護。這樣看來咱們每次在不一樣線程調用ThreadLocalset方法set的數據是存在不一樣線程的ThreadLocalMap中的,就像註釋說的ThreadLocal只是起了個維護ThreadLocalMap的功能。想到是get方法一樣也是到不一樣線程的ThreadLocalMap去取數據。
get方法:生命週期

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();
}
複製代碼

果真,get方法中一樣是先獲取當前線程對象,而後在拿着這個對象t去獲取到t中的ThreadLocalMap,只要map不等於null就調用map.getEntry(this)方法來獲取數據,由於ThreadLocalMap裏使用一個內部類Entry來存儲數據的,因此調用getEntry(this)方法,傳入的key是當前的ThreadLocal。這樣獲取到Entry類型數據e,只要e不爲null,返回e.value即先前存儲的數據。若是獲取到的mapnull又或者根據key獲取Entrynull,就調用setInitialValue方法初始化一個value返回。
setInitialValueinitialValue方法:內存

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;
}
複製代碼

setInitialValue方法中首先調用initialValue方法初始化了一個空value,以後的操做和set方法相同,將這個空的value加入到當前線程的ThreadLocalMap中去,ThreadLocalMap爲空就建立個Map,最後返回這個空值。
至此,ThreadLocalgetset方法就都看過了,也理解了ThreadLocal能夠在多個線程中操做而互不干擾的緣由。可是ThreadLocal還有一個要注意的地方就是ThreadLocal使用不當會形成內存泄漏。

五、ThreadLocal內存泄漏的緣由

內存泄漏的根本緣由是當一個對象已經不須要再使用本該被回收時,另一個正在使用的對象持有它的引用從而致使它不能被回收,致使本該被回收的對象不能被回收而停留在堆內存中。那麼ThreadLocal中是在哪裏發生的呢?這就要看到ThreadLocalMap中存儲數據的內部類Entry

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

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

能夠看到這個Entry類,這裏的key是使用了個弱引用,因此由於使用弱引用這裏的keyThreadLocal會在JVM下次GC回收時候被回收,而形成了個keynull的狀況,而外部ThreadLocalMap是沒辦法經過null key來找到對應value的。若是當前線程一直在運行,那麼線程中的ThreadLocalMap也就一直存在,而map中卻存在key已經被回收爲null對應的Entryvalue卻一直存在不會被回收,形成內存的泄漏。
不過,這一點設計者也考慮到了,在get()set()remove()方法調用的時候會清除掉線程ThreadLocalMap中全部EntryKeynullValue,並將整個Entry設置爲null,這樣在下次回收時就能將Entryvalue回收。
這樣看上去好像是由於key使用了弱引用才致使的內存泄漏,爲了解決還特地添加了清除null key的功能,那麼是否是不用弱引用就能夠了呢?
很顯然不是這樣的。設計者使用弱引用是由緣由的。

  • 若是使用強引用,那麼若是在運行的線程中ThreadLocal對象已經被回收了可是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,一樣致使內存泄漏。
  • 若是使用弱引用ThreadLocal的對象被回收了,由於ThreadLocalMap持有的是ThreadLocal的弱引用,即便沒有手動刪除,ThreadLocal也會被回收。nullkeyvalue在下一次ThreadLocalMap調用setgetremove的時候會被清除。

因此,因爲ThreadLocalMap和線程Thread的生命週期同樣長,若是沒有手動刪除Map的中的key,不管使用強引用仍是弱引用實際上都會出現內存泄漏,可是使用弱引用能夠多一層保護,null key在下一次ThreadLocalMap調用setgetremove的時候就會被清除。 所以,ThreadLocal的內存內泄漏的真正緣由並不能說是由於ThreadLocalMap的key使用了弱引用,而是由於ThreadLocalMap和線程Thread的生命週期同樣長,沒有手動刪除Map的中的key纔會致使內存泄漏。因此解決ThreadLocal的內存泄漏問題就要每次使用完ThreadLocal,都要記得調用它的remove()方法來清除。

參考資料:

Android開發藝術探索

ThreadLocal內存泄漏真因探究

相關文章
相關標籤/搜索