深入理解ThreadLocal

前言:初识ThreadLocal这个类,还要追溯到Handler的源码分析,之后翻看任主席的《Android艺术开发探索》时再次与ThreadLocal谋面,当时对于这个类的认知很简单,就是:ThreadLocal为变量在每个线程中都创建了一个副本,每个线程都可以独立访问相应的副本变量的值,各个线程之间的访问互不影响。近期有时间翻看了下ThreadLocal的源码,来加深自己对ThreadLocal的理解,下面我们一起来学习一下。

首先我们先看一个小Demo:

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void mTest(View view){

        mBooleanLocal.set(true);
        Log.e("--------Thread#UI------",String.valueOf(mBooleanLocal.get()));

        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.e("---------Thread#1------",String.valueOf(mBooleanLocal.get()));
            }
        },"Thread#1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                mBooleanLocal.set(false);
                Log.e("---------Thread#2------",String.valueOf(mBooleanLocal.get()));
            }
        },"Thread#2").start();
    }
}

可以看到在MainActivity中我们只放置了一个Button按钮,mTest()方法为按钮的点击事件。点击按钮我们看下打印的日志:

07-30 13:56:32.536 15557-15557/com.example.halobear.threadlocaltest E/--------Thread#UI------: true
07-30 13:56:32.538 15557-16994/com.example.halobear.threadlocaltest E/---------Thread#1------: null
07-30 13:56:32.539 15557-16995/com.example.halobear.threadlocaltest E/---------Thread#2------: false

通过打印的日志我们可以看到,UI线程中打印的结果为true,Thread#1线程中打印的结果为null,Thread#2线程中打印的结果为false。为什么会这样子呢???让我们抱着疑惑点开ThreadLocal的源码。

我们首先看下ThreadLocal的构造方法:

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

are you kidding me ? ? ?,构造方法中竟然是一个空实现。不要着急,我们顺着源头慢慢找,接着我们看下ThreadLocal的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);
    }

我们先简单说下:第一步,首先获取到当前线程,然后调用getMap方法,将当前线程对象传入,获取到当前线程对应的ThreadLocalMap,最后判断map是否为null,当map不为null时,调用map的set方法来存储数据(注意这里的key为this,即当前ThreadLocal对象),当map为null时,调用creatMap方法来创建map并进行数据存储。
我们点进去getMap方法看下:

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

可以看到方法中直接返回了 t.threadLocals,t就是我们刚才传入的当前线程对象,threadLocals是什么东西,我们去Thread类中看一下:

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

可以看到threadLocals就是我们Thread类的一个成员变量。ThreadLocal对于数据的存储,实质上是将数据存储在每个线程对象的成员变量threadLocals中。我们通过ThreadLocal来获取数据时,首先会获取到当前线程,进而获取到当前线程对象threadLocals成员变量,进而进行数据获取操作。
我们首次调用ThreadLocal的set方法时,通过getMap获取到的ThreadLocalMap肯定为null,那么程序就会走到else语句,进行ThreadLocalMap的创建以及初始化等操作。我们点击createMap方法来看一下:

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

createMap方法中果然对ThreadLocalMap对象进行了创建,我们点进去看下ThreadLocalMap的构造方法(注:ThreadLocalMap为ThreadLocal类的静态内部类):

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

可以看出:ThreadLocalMap以我们的ThreadLocal对象为key,以我们要存储的数据为value,内部是通过动态数组来实现的。INITIAL_CAPACITY为该数组的初始大小,setThreshold方法设置了扩容的临界点。具体源码我在这里就不贴出来了,有兴趣的话大家自行翻看。

上面我们分析了首次调用ThreadLocal的set方法时的操作,下面我们分析下后续调用set方法时的情形,这个时候map肯定不为null,方法会走到ThreadLocalMap的set方法,我们点进去看下:

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

            // 1. 获取下标
            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) {
                    //2. 如果 k == key,对value值进行覆写
                    e.value = value;
                    return;
                }

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

            // 3. 在table数组下标 i 处创建新节点,进行赋值操作(一个线程中对应多个ThreadLocal对象时)
            tab[i] = new Entry(key, value);
            int sz = ++size;

            if (!cleanSomeSlots(i, sz) && sz >= threshold)

                //4. 当前元素个数大于等于threshold临界点时,进行扩容操作
                rehash();
        }

关键点地方在上述代码中已经标注了。

ThreadLocal的set方法我们大致分析完毕了,接下来我们看下ThreadLocal的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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可以看出在ThreadLocal的get方法中,首先获取到当前线程,进而获取到当前线程对象threadLocals成员变量,接下来对map进行判断,如果map不为空,则调用map的getEntry方法,获取到Entry元素,进而获取到value值。如果map为null,则调用setInitialValue()方法。我们对map为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()方法,我们点进去看下:

    protected T initialValue() {
        return null;
    }

可以看出initialValue()方法直接return null。其实这个方法是用来初始化value操作的。当我们没有调用ThreadLocal的set方法,直接调用get方法的时候,这种情况下map肯定为null,最后程序会调用到initialValue方法,将initialValue方法中的返回值直接return出去。这也就是在我们的示例Demo中,Thread#1线程打印结果为null的原因。我们创建ThreadLocal对象的时候可以重写该方法,用来对value值进行初始化操作。

在上述ThreadLocal的get方法中,不知道你有没有注意到这样一个细节,为什么在get方法中已经对map进行了null判断,后续在setInitialValue()方法中再一次对map进行null判断???我的理解是,这样子做是考虑到一个线程中创建多个ThreadLocal对象这种情况。当一个线程中只有一个ThreadLocal对象的时候,如果我们没有调用ThreadLocal的set方法,直接调用到ThreadLocal对象的get方法,这个时候程序在get方法中对map进行判断肯定为null,程序走到setInitialValue方法中,接下来再次对map进行判断,map肯定为null,很显然走else语句块中的createMap方法。

接下来我们考虑下一个线程中创建多个ThreadLocal对象的情况。假设我们在一个线程中创建了两个ThreadLocal对象,分别为localOne和localTwo,如下所示:

        new Thread(new Runnable() {
            @Override
            public void run() {

                ThreadLocal<Integer> localOne = new ThreadLocal<>();
                ThreadLocal<String> localTwo = new ThreadLocal<>();

                localOne.set(2);
                Log.e("---------Thread------",String.valueOf(localOne.get()));

                Log.e("---------Thread------",String.valueOf(localTwo.get()));
            }
        }).start();

我们首先创建了localOne对象,并调用了它的set方法和get方法,接着我们创建了localTwo对象,直接调用它的get方法。注意:虽然localOne和localTwo为两个不同的对象,但都在同一个线程中创建,它们所依附的threadLocals成员变量是同一个,也可以说map为同一个。我们对localTwo“直接调用get方法”进行分析,在get方法中首先对map进行判断,因为localTwo的执行顺序在localOne之后,此时map肯定不为null,获取到的Entry元素e为null,程序还是走到setInitialValue()方法中,再次对map进行判断,map不为null,这个时候就会走到map的set方法。

关于ThreadLocal的分析就到这里了,我们下期再会哈哈。

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

推荐阅读更多精彩内容