深入分析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/

 

 本文由用戶 執著夢飛 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!