Android 之 ThreadLocal简析

前言

源码 基于 Android SDK 28 JDK 1.8


说起 ThreadLocal,大家可能会比较陌生,但是如果想要比较好地理解 Android 的消息机制,ThreadLocal 是必须要掌握的,这是因为 Looper 的工作原理,就跟 ThreadLocal 有很大的关系,理解 ThreadLocal 的实现方式有助于我们理解 Looper 的工作原理,这篇文章就从 ThreadLocal 的用法讲起,一步一步带大家理解 ThreadLocal。

一、ThreadLocal 是什么


ThreadLocal 是一个线程内部的数据存储类,通过它可以在 指定的线程中 存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。

一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。

二、基本用法


创建,支持泛型

ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();

set 方法

mBooleanThreadLocal.set(false);

get 方法

mBooleanThreadLocal.get()

接下来用一个完整的例子,帮助大家理解 ThreadLocal



public class MainActivity extends AppCompatActivity {

    String TAG = "ThreadLocal";
    ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBooleanThreadLocal.set(true);
        Log.d(TAG, "Current Thread: mBooleanThreadLocal is : " + mBooleanThreadLocal.get());
        new Thread("Thread#1") {
            @Override
            public void run() {
                super.run();
                mBooleanThreadLocal.set(false);
                Log.d(TAG, "Thread 1: mBooleanThreadLocal is : " + mBooleanThreadLocal.get());
            }
        }.start();
        new Thread("Thread#2") {
            @Override
            public void run() {
                super.run();
                Log.d(TAG, "Thread 2: mBooleanThreadLocal is : " + mBooleanThreadLocal.get());
            }
        }.start();

    }
}

在上面的代码中,在主线程中设置 mBooleanThrealLocal 的值为 true,在子线程 1 中设置为 false,在子线程 2 中不设置 mBooleanThrealLocal 的值,然后分别在 3 个线程中通过 get() 方法获取 mBooleanThrealLocal 的值


从上面的日志中可以看出,虽然在不同的线程中访问的是同一个 ThrealLocal 对象,但是它们通过 ThrealLocal 获取到的值确实不一样的,这就是 ThrealLocal 的奇妙之处了。

ThrealLocal 之所以有这么奇妙的效果,就是因为不同线程访问同一个 ThrealLocal 的 get() 方法,ThrealLocal 内部都会从各自的线程中取出一个数组,然后再从数组中根据当前 ThrealLocal 的索引去查找不同的 value 值。

三、ThrealLocal 工作原理


ThrealLocal 是一个泛型类,它的定义为 public class ThrealLocal<T>,只要弄清楚 ThrealLocal 的 set() 和 get() 方法就可以明白它的工作原理了。

1、ThrealLocal 的 set() 方法

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    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(Thread t) 方法获取一个 ThreadLocalMap,如果这个 map 不为空的话,就将 ThrealLocal 和 我们想存放的 value 设置进去,不然的话就创建一个 ThrealLocalMap 然后再进行设置。

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocalMap 其实是 ThreadLocal 里面的静态内部类,而每一个 Thread 都有一个对应的 ThrealLocalMap,因此获取当前线程的 ThrealLocal 数据就变得异常简单了。

public class Thread implements Runnable {

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

下面看一下,ThrealLocal 的值到底是如何在 threadLocals 中进行存储的。
首先我们先看一下set方法中的

map.set(this, value);

对应源码为:

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

可以看到set方法中会获取Entry对象,然后判断当前threadlocal对象是否存储在table数组中,若存在则更新value,若不存在,则新增一个Entry对象'''new Entry(key, value);'''存储threadlocal和对应的值,然后加入到table数组中。
在 threadLocals 内部有一个数组,private Entry[] table,ThrealLocal 的值就存在这个 table 数组中。


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

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;


2、ThrealLocal 的 get() 方法

上面分析了 ThreadLocal 的 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() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

可以发现,ThrealLocal 的 get() 方法的逻辑也比较清晰,它同样是取出当前线程的 threadLocals 对象,如果这个对象为 null,就调用 setInitialValue() 方法

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial 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;
    }

在 setInitialValue() 方法中,将 initialValue() 的值赋给我们想要的值,默认情况下,initialValue() 的值为 null,当然也可以重写这个方法。

   /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the <tt>initialValue</tt> method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns <tt>null</tt>; if the
     * programmer desires thread-local variables to have an initial
     * value other than <tt>null</tt>, <tt>ThreadLocal</tt> must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

如果 threadLocals 对象不为 null 的话,那就取出它的 table 数组并找出 ThreadLocal 的 reference 对象在 table 数组中的位置。

从 ThreadLocal 的 set() 和 get() 方法可以看出,他们所操作的对象都是当前线程的 threalLocals 对象的 table 数组,因此在不同的线程中访问同一个 ThreadLocal 的 set() 和 get() 方法,他们对 ThreadLocal 所做的 读 / 写 操作权限仅限于各自线程的内部,这就是为什么可以在多个线程中互不干扰地存储和修改数据。

总结

ThreadLocal 是线程内部的数据存储类,每个线程中都会保存一个 ThreadLocal.ThreadLocalMap threadLocals = null;,ThreadLocalMap 是 ThreadLocal 的静态内部类,里面保存了一个 private Entry[] table 数组,这个数组就是用来保存 ThreadLocal 中的值。通过这种方式,就能让我们在多个线程中互不干扰地存储和修改数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容