昨天在CVTE的面试中被面试官问到ThreadLocal的实现原理,我支支吾吾回答(其实就是不知道)的不是很好,今天通过翻书加看博客,基本掌握了ThreadLocal的实现原理,故而在本片博客进行一个总结,希望能给大家带来一些收获。

首先ThreadLocal是什么?

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

作为一个学习安卓的菜鸡,ThreadLocal在Android中和我碰面的机会不多,像那些AMS中和ActivityThread中的用法我就不写出来装逼了(主要我也不是很懂),接触最多的用法就是在handler进行线程间通信的时候用来保存每个线程创建的Looper对象。

当我刚开始搜博客看ThreadLocal的实现原理时看的我是一头雾水,我陆陆续续看到了两种实现原理,第一种,内部的实际存储类是ThreadLocalMap,另一种内部的实际存储类是values,这让我百思不得其解,直到我看到了一篇大神的博客,才让我如漫漫黑夜里找到了一座灯塔。原来如此:

android版的ThreadLocal和java原生的ThreadLocal有一定的差别,android版的进行了一些优化设计,通过内部类Values中的Object数组来存储ThreadLocal的弱引用和线程的局部数据对象;而java版的是以MAP的方式来存储。

那下面我就分别来分析一下两种ThreadLocal。

1.Android中的ThreadLocal

ThreadLocal的使用很简单,定义一个全局的ThreadLocal对象,在线程中通过get()方法得到当前线程的局部数据进行操作。

ThreadLocal<ClassType> storageDataThreadLocal = new ThreadLocal<ClassType>(){
    @Override
    protected ClassType initialValue() {
        return new ClassType();
    }
};

 

ClassType为自定义的数据类,实例化ThreadLocal对象过程中如果没有重写initialValue方法则第一次调用get方法会返回空,这里需要注意。

下面分析一下ThreadLocal的源码:

我们直接分析get函数:

public T get() {
    // Optimized for the fast path.
    //通过native方法得到代码当前执行的线程对象
    Thread currentThread = Thread.currentThread();
    //得到当前线程的Values类型对象
    Values values = values(currentThread);
    if (values != null) {
          //得到对象数组table,用来存储当前ThreadLocal对象的弱引用
          //和当前线程的局部数据对象引用
        Object[] table = values.table;
        //得到用于存储ThreadLocal弱引用的散列值索引
        int index = hash & values.mask;
        //指向到对象相同,则返回其引用
        if (this.reference == table[index]) {
            return (T) table[index + 1];
        }
    } else {
          //新建values对象赋给当前线程对象的localValues引用
        values = initializeValues(currentThread);
    }

    return (T) values.getAfterMiss(this);
}

 

如果当前线程对象内的Values对象中有存储和当前ThreadLocal对象的reference属性是同一个对象的引用则返回此线程的本地数据,即变量在线程的本地拷贝。

否则在当前线程内部创建一个Values对象,通过Values.getAfterMiss将ThreadLocal类的initialValue方法得到的本地数据存储到当前线程对象内部并返回;initialValue是protected方法,如果子类没有重写,则默认返回空。

静态内部类Values的getAfterMiss函数:

Object getAfterMiss(ThreadLocal<?> key) {
    Object[] table = this.table;
    //通过散列算法得到ThreadLocal的first slot的索引值
   int index = key.hash & mask;

   // If the first slot is empty, the search is over.
   if (table[index] == null) {
     //如果first slot上没有存储 则将ThreadLocal的弱引用和本地数据
     //存储到table数组的相邻位置并返回本地数据对象引用
       Object value = key.initialValue();

       // If the table is still the same and the slot is still empty...
       if (this.table == table && table[index] == null) {
             table[index] = key.reference;
             table[index + 1] = value;
             size++;

             cleanUp();
             return value;
         }

         // The table changed during initialValue().
         //遍历table数组,根据不同判断将ThreadLocal的
         //弱引用和本地数据对象引用存储到table数组的相应位置
         put(key, value);
         return value;
     }

     // Keep track of first tombstone. That's where we want to go back
     // and add an entry if necessary.
     int firstTombstone = -1;

     // Continue search.
     for (index = next(index);; index = next(index)) {
         Object reference = table[index];
         if (reference == key.reference) {
             return table[index + 1];
         }

         // If no entry was found...
         if (reference == null) {
             Object value = key.initialValue();

             // If the table is still the same...
             if (this.table == table) {
                 // If we passed a tombstone and that slot still
                 // contains a tombstone...
                 if (firstTombstone > -1
                         && table[firstTombstone] == TOMBSTONE) {
                     table[firstTombstone] = key.reference;
                     table[firstTombstone + 1] = value;
                     tombstones--;
                     size++;

                     // No need to clean up here. We aren't filling
                     // in a null slot.
                     return value;
                 }

                 // If this slot is still empty...
                 if (table[index] == null) {
                     table[index] = key.reference;
                     table[index + 1] = value;
                     size++;

                     cleanUp();
                     return value;
                 }
             }

             // The table changed during initialValue().
             put(key, value);
             return value;
         }

         if (firstTombstone == -1 && reference == TOMBSTONE) {
             // Keep track of this tombstone so we can overwrite it.
             firstTombstone = index;
         }
     }
 }

getAfterMiss函数根据不同的判断将ThreadLocal的弱引用和当前线程的本地对象以类似MAP的方式,存储在table数组的相邻位置,其中其散列的索引hash值是通过hashCounter.getAndAdd(0x61c88647 * 2)算法来得到。

set函数:

public void set(T value) {
    Thread currentThread = Thread.currentThread();
    //得到当前线程的本地Values对象
    Values values = values(currentThread);
    if (values == null) {
          //为空则新建Values对象
        values = initializeValues(currentThread);
    }
    //将当前ThreadLocal对象引用和本地数据存储到
    //当前ThreadLocal的内部Values对象的table数组当中
    //当调用get方法时,获得的即是当前线程的本地数据
    values.put(this, value);
}

分析到这大家应该对Android中的ThreadLocal类有了自己的理解了吧?

总结:

ThreadLocal实现线程本地存储的原理是比较清晰的,即在当前线程中调用get方法时,通过ThreadLocal的initialValue方法创建当前线程的一个本地数据拷贝,将此拷贝添加到当前线程本地数据的table数组当中;或者在调用set方法时,将当前线程的本地数据存储到当前线程的table数组中.当前线程通过调用ThreadLocal对象的get方法即得到当前线程本地数据对象。

下面说一下Android的ThreadLocal的内存泄漏问题:

java版的ThreadLocal“value值因为存在一条从current thread连接过来的强引用,只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.”.

在android版的ThreadLocal中value值不存在强引用,故在table数组中的指定位置置为null,本地数据则没有存在指向其的引用,当GC回收的时候会清理掉。

2.Java中的ThreadLocal

 

先了解一下Java的ThreadLocal类提供的几个方法:

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本

public void set(T value) { } //set()用来设置当前线程中变量的副本

public void remove() { } //remove()用来移除当前线程中变量的副本

protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的 
 

ThreadLocal类中的get方法源码实现如下:

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();  

}

第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的 ,所以通过ThreadLocalMap的Entry获取到<key,value>键值对 如果获取成功,则返回value值。 如果map为空,则调用setInitialValue方法返回value。
 

首先看一下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;  

}

在getMap中,是调用当期线程t,返回当前线程t中的一个成员变量threadLocals

Thread类中的成员变量threadLocals是什么?源码如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

原来threadLocals是一个ThreadLocalMap,这个类型是ThreadLocal类的一个内部类,ThreadLocalMap的实现如下:

static class ThreadLocalMap {

...

        static class Entry extends WeakReference<ThreadLocal> {

            /** The value associated with this ThreadLocal. */

            Object value;

 

            Entry(ThreadLocal k, Object v) {

                super(k);

                value = v;

            }

        }

...

}

可以看到ThreadLocalMap的Entry继承了WeakReference,并且使用ThreadLocal作为键值。

 

接着从get()方法分析,如果map为空,则调用setInitialValue方法返回value。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;  

   } 

 

    /**

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

    }

如果map不为空,就设置键值对,如果map为空,调用createMap,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 

 * @param map the map to store. 

 */  

void createMap(Thread t, T firstValue) {  

    t.threadLocals = new ThreadLocalMap(this, firstValue);  

}

ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取,每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
这个时候再看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);  

   }

在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,其中以this指向的threadlocal对象为健值,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。
 

JAVA中ThreadLocal的总结:

1)实际通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量;

3)在进行get之前,必须先set,否则会报空指针异常;因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。    

如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

注意点:

一个线程可以创建 多个ThreadLocal对象

JAVA中ThreadLocal的实际副本存储是ThreadLocal.ThreadLocals,这个ThreadLocals实际就是一个ThreadLocalMap。

😊😊😊😊

好了,到这我们就把JAVA中和Android中的两种不同的ThreadLocal给分析完了,相信大家和我一样收获满满!!!😊

😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊😊