Android進階知識:ThreadLocal

胖宅老鼠發表於2019-04-05

1、ThreadLocal是什麼?

ThreadLocal是一個執行緒內部資料儲存類,通過他可以在指定的執行緒中儲存資料。儲存後,只能在指定的執行緒中獲取到儲存的資料,對其他執行緒來說無法獲取到資料。

2、ThreadLocal的使用場景

日常使用場景不多,當某些資料是以執行緒為作用域並且不同執行緒具有不同的資料副本的時候,可以考慮使用ThreadLocalAndroid原始碼的LopperActivityThread以及AMS中都用到了ThreadLocal

3、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();
}
}
複製程式碼

執行結果:

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

可以看到雖然訪問的是同一個ThreadLocal物件,但是獲取到的值卻是不一樣的。

4、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方法:

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使用不當會造成記憶體洩漏。

5、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記憶體洩漏真因探究

相關文章