深入理解ThreadLocal

作者: Android_Jian | 来源:发表于2018-07-30 19:48 被阅读17次

    前言:初识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的分析就到这里了,我们下期再会哈哈。

    相关文章

      网友评论

        本文标题:深入理解ThreadLocal

        本文链接:https://www.haomeiwen.com/subject/afcfvftx.html