深入分析ThreadLocal
這篇文章主要分析Android中的ThreadLocal原理以及相關問題, 也分析與其在Java中內部實現的區別, 讓大家理解ThreadLocal的使用場景與正確使用方法.
ThreadLocal的定義
Android源碼中描述:
Implements a thread-local storage, that is, a variable for which each thread
has its own value. All threads share the same {@code ThreadLocal} object,
but each sees a different value when accessing it, and changes made by one
thread do not affect the other threads. The implementation supports
{@code null} values.
實現了線程局部變量的存儲. 所有線程共享同一個ThreadLocal對象, 但是每個線程只能訪問和修改自己存儲的變量, 不會影響其他線程. 此實現支持存儲null變量.
從上面的定義看出, 關鍵的地方即: ThreadLocal對象是多線程共享的, 但每個線程持有自己的線程局部變量. ThreadLocal不是用來解決共享對象問題的, 而是提供線程局部變量, 讓線程之間不會互相干擾.
下面看在Android中Looper的應用, 每個線程只有一個Looper對象:
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/
public static Looper myLooper() {
return sThreadLocal.get();
}
在了解ThreadLocal的作用后, 也會產生一些疑問:
線程局部變量是怎么存儲的?
是怎么做到線程間相互獨立的?
接下來在分析Android的ThreadLocal源碼的過程中, 理解其實現原理, 并解決上面的疑問.
ThreadLocal的實現原理
ThreadLocal有三個主要的public方法: set, get, remove.
ThreadLocal是通過set方法存儲局部變量的, 所以先從set方法看起:
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
/**
* Gets Values instance for this thread and variable type.
*/
Values values(Thread current) {
return current.localValues;
}
/**
* Creates Values instance for this thread and variable type.
*/
Values initializeValues(Thread current) {
return current.localValues = new Values();
}
set方法中根據當前線程獲得 Values , 線程局部變量也是存儲在 Values 中, 而不是ThreadLocal對象中. 如果一開始values為null, 就通過initializeValues方法初始化. 上面代碼根據線程獲得的 values 變量就是Thread對象的localValues變量, 可看下Thread源碼中相關部分:
public class Thread implements Runnable {
/**
* Normal thread local values.
*/
ThreadLocal.Values localValues;
}
接下來來看Values的定義, 了解其內部結構, 進一步清楚線程局部變量的存儲細節:
/**
* Per-thread map of ThreadLocal instances to values.
*/
static class Values {
...
/**
* Map entries. Contains alternating keys (ThreadLocal) and values.
* The length is always a power of 2.
*/
private Object[] table;
...
}
Values是用數組來存儲ThreadLocal和對應的value的, 保存一個線程中不同ThreadLocal以及局部變量.Values類內部具體的細節, 推薦閱讀 由淺入深全面剖析ThreadLocal . 其實table數組中沒有保存ThreadLocal的強引用, 而是ThreadLocal的reference變量, 實際上就是保存ThreadLocal的弱引用.
/** Weak reference to this thread local instance. */
private final Reference<ThreadLocal<T>> reference
= new WeakReference<ThreadLocal<T>>(this);
到這里就可以回答之前提到的兩個問題, 線程局部變量是存儲在Thread的localValues屬性中, 以ThreadLocal的弱引用作為key, 線程局部變量作為value. 雖然每個線程共享同一個ThreadLocal對象, 但是線程局部變量都是存儲在線程自己的成員變量中, 以此保持相互獨立.
ThreadLocal的get方法的默認值
get方法就是取出Vaules中對應的線程局部變量, 需要注意的是在沒有set的情況下, 調用get方法返回的默認值是null, 這其實是有initialValue方法確定的, 可以重寫.
/**
* Provides the initial value of this variable for the current thread.
* The default implementation returns {@code null}.
*
* @return the initial value of the variable.
*/
protected T initialValue() {
return null;
}
Java中的ThreadLocal有什么區別
Java的ThreadLocal源碼與Android中的ThreadLocal不太一樣, 不過大致的實現原理是一樣的, Android中ThreadLocal稍微優化了一下, 更節約內存. 兩者最大的區別就是存儲局部變量的Values類在Java中是ThreadLocalMap類, 內部的存儲方式有些不同, Java中用ThreadLocal.ThreadLocalMap.Entry來封裝key和value.
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16;
private ThreadLocal.ThreadLocalMap.Entry[] table;
private int size;
private int threshold;
...
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
this.value = v;
}
}
}
兩者都是以ThreadLocal弱引用作為key值.
ThreadLocal的內存泄漏問題
網上有討論說ThreadLocal有可能出現內存泄漏問題, 這的確是有可能的.
現在看下線程局部變量的引用鏈: Thread.localValues -> WeakReference 和 value. 如果沒有其他對象引用ThreadLocal對象的話, ThreadLocal可能會被回收, 但是value不會被回收, value是強引用. 所以沒有顯式地調用remove的話, 的確有可能發生內存泄漏問題.
不過ThreadLocal的設計者也考慮到這個問題, 在get或set方法中會檢測key是否被回收, 如果是的話就將value設置為null, 具體是調用Values的cleanUp方法實現的. 這種設計可以避免多數內存泄漏問題, 但是極端情況下, ThreadLocal對象被回收后, 也沒有調用get或set方法的話, 還是會發生內存泄漏.
現在回過來看, 這種情況的發生都是基于沒有調用remove方法, 而ThreadLocal的正確使用方式是在不需要的時候remove, 這樣就不會出現內存泄漏的問題了.
線程局部變量真的只能被一個線程訪問?
ThreadLocal的子類InheritableThreadLocal可以突破這個限制, 父線程的線程局部變量在創建子線程時會傳遞給子線程.
看下面的示例, 子線程可以獲得父線程的局部變量值:
private void testInheritableThreadLocal() {
final ThreadLocal<String> threadLocal = new InheritableThreadLocal();
threadLocal.set("testStr");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i(LOGTAG, "testInheritableThreadLocal = " + threadLocal.get());
}
};
t.start();
}
// 輸出結果為 testInheritableThreadLocal = testStr
具體的實現邏輯:
public class Thread implements Runnable {
...
/**
* Normal thread local values.
*/
ThreadLocal.Values localValues;
/**
* Inheritable thread local values.
*/
ThreadLocal.Values inheritableValues;
...
// 線程創建會調用的方法
private void create(ThreadGroup group, Runnable runnable, String threadName, long stackSize) {
...
// Transfer over InheritableThreadLocals.
if (currentThread.inheritableValues != null) {
inheritableValues = new ThreadLocal.Values(currentThread.inheritableValues);
}
// add ourselves to our ThreadGroup of choice
this.group.addThread(this);
}
}
使用建議
-
ThreadLocal變量本身定位為要被多個線程訪問, 所以通常定義為static
-
在線程池的情況下, 在ThreadLocal業務周期結束后, 最好顯示地調用remove方法
來自:http://johnnyshieh.github.io/android/2016/11/02/explore-threadlocal/