Android消息机制之线程间存储ThreadLocal源码分析

96
皮球侧飞
0.1 2016.10.27 15:59* 字数 1285

概述

ThreadLocal是一种线程内部存储类。通过他存储的数据在不同的线程操作而互不干扰,类似于各个线程中都有一份自己的copy数据做处理。正如源码中的注解解释:

This class provides thread-local variables.

Android消息机制中就用到了该类来存储每个线程的Looper。
概念可能有些抽象,下面举一个简单的例子:

private final String TAG = "MainActivity";
private ThreadLocal<String> threadLocal = new ThreadLocal<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    threadLocal.set("mainThread");
    Log.d(TAG, "this is mainThread ,threadLocal = " + threadLocal.get());
    new Thread("Thread_1") {
        @Override
        public void run() {
            super.run();
            Log.d(TAG, "this is Thread_1 ,before calling set method , threadLocal =     " + threadLocal.get());
            threadLocal.set("Thread_1");
            Log.d(TAG, "this is Thread_1 ,after calling set method , threadLocal =      " + threadLocal.get());
        }
    }.start();

    new Thread("Thread_2") {
        @Override
        public void run() {
            super.run();
            Log.d(TAG, "this is Thread_2 ,before calling set method , threadLocal =     " + threadLocal.get());
            threadLocal.set("Thread_2");
            Log.d(TAG, "this is Thread_2 ,after calling set method , threadLocal =      " + threadLocal.get());
        }
    }.start();
}

上述实例代码中在MainThread中给ThreadLocal赋值了“mainThread”,并打印结果。然后在“Thread_1”和“Thread_2”中打印输出ThreadLoal的值,再重新赋值打印。输出结果如下:

this is mainThread ,threadLocal = mainThread this is Thread_2 ,before calling set method , threadLocal = null this is Thread_2 ,after calling set method , threadLocal = Thread_2 this is Thread_1 ,before calling set method , threadLocal = null this is Thread_1 ,after calling set method , threadLocal = Thread_1

从上述日志可以看到,3个不同的线程虽然都在访问同一个ThreadLoal,但是获取的值是不同的。相当于每个线程都有属于自己的一个ThreadLoal副本,各自线程中互不干扰。

ThreadLoal源码分析

ThreadLoal的构造方法是个空方法,直接来看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();
    //根据当前线程获取ThreadLoacalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}


ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
* 新建ThreadLocalMap
*/
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}



//Thread.java

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

注解中指出,该方法是把值赋值给当前线程的局部变量副本。此处代码逻辑比较清晰,如果ThreadLoacalMap不为空则将set操作转移给了ThreadLocalMap,否则创建一个ThreadLoacalMap。ThreadLocalMap是ThreadLoacal的静态内部类,在每个Thread类中都已经定义了初始值为null的ThreadLoacalMap。先来分析下ThreadLocalMap的构造方法:

//初始容量
private static final int INITIAL_CAPACITY = 16;


ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
      table = new Entry[INITIAL_CAPACITY];
      //根据key的hash进行数组坐标的相关计算
      int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
      table[i] = new Entry(firstKey, firstValue);
      size = 1;
      //设置阈值
      setThreshold(INITIAL_CAPACITY);
}

static class Entry extends WeakReference<ThreadLocal> {
   /** The value associated with this ThreadLocal. */
   Object value;

   Entry(ThreadLocal k, Object v) {
       super(k);
       value = v;
    }
}

从上面代码可以看出,ThreadLoacalMap定义了一个名为table的Entry[]数组,创建一个Entry对象,根据key的相关hash运算得到数组下标,存储在table中。ThreadLocalMap的set方法便是在构造方法的基础上对重复元素进行一些处理:

 private void set(ThreadLocal key, Object value) {
   Entry[] tab = table;
   int len = tab.length;
   int i = key.threadLocalHashCode & (len-1);
   //从i开始向后遍历,查看table中是否已经存放过该key的数据或是table中有key为null的
   for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
        ThreadLocal k = e.get();
            //更新value
            if (k == key) {
                e.value = value;
                return;
            }
            //替换该位置上的Entry
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
   //table中没有空位置且该数据为新增Entry
   tab[i] = new Entry(key, value);
   int sz = ++size;
   //是否需要扩容
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
      rehash();
}

从以上方法可以看出,操作的数据是以ThreadLoacal.ThreadLoacalMap.Entry对象的形式存储,而实际才存储位置是在table数组中。ThreadLoacalMap的set方法和ThreadLoacalMap的的构造方法一样,同样是根据key来计算存放在数组中的下标,便从该下标i开始向后遍历。如果存在该key则进行value的update。如果该下标位置是个空位置(因为Entry采用的是WeakReference弱引用,如果被回收了则不需要再存储改数据)则直接替换成我们新set的数据,而replaceStaleEntry方法里面则执行了一些回收替换操作。如果table中没有空位置且该数据为新增Entry,那么就插入一个新数据,为了防止数组越界,需要对存储容量做计算,判断是否需要扩容和重包装。threshold就是扩容或是重包装的临界点:
threshold= len * 2 / 3;
rehash()方法则进行table的重包装和重新设置大小的:

private void rehash() {
  //删除一些旧的的Entry
  expungeStaleEntries();

  // Use lower threshold for doubling to avoid hysteresis
  if (size >= threshold - threshold / 4)
       resize();
}

上述代码中看到table做了一些删除工作,如果存储量达到了3/4就要去重新计算存储空间。resize方法对table的容量扩充了一倍并重新设定了threshold的值。上面分析了ThreadLocal的set方法,这里分析下它的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();
}

可以发现,ThreadLocal的get方法的逻辑比较清晰,它取出当前线程的ThreadLocalMap.Entry对象,如果这个对象为null那么就返回初始值,初始值由ThreadLocal的setInitialValue方法来描述,默认情况下为null,开发者可以根据需要去重写该方法。而这个获取的ThreadLocalMap.Entry就是set方法存储在table中的数据,直接根据key计算出来下标从table数组中去获取,ThreadLoacal还针对数据有可能丢失的情况做了处理:

 private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal k = e.get();
            if (k == key)
                return e;
            if (k == null)
                expungeStaleEntry(i);
            else
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }

上述代码逻辑也比较清晰了,ThreadLoacal会对table向后遍历,查找是否有对应的数据,整改table中都没有才会返回null。

从ThreadLocal的set和get方法可以看出,它们所操作的对象都是当前所在线程的ThreadLoacalMap对象中的table数组,所以在不同线程中访问同一个ThreadLocal的set和get方法,它们对ThreadLocal所做的读写操作都是在各自线程的内部,这就能解释为什么ThreadLocal在多个线程中存储和修改数据都是互不干扰地的了。在Android的消息处理机制中,便用了ThreadLoacal来存储Looper。

从ThreadLoacal的角度来说,ThreadLoacal只是数据存储/获取事件的分发者,在哪个Thread中调用就将数据派发到哪个Thread中,实际存储/获取还是在各种Thread中。而从Thread的角度来说,ThreadLoacal只是存储的key,根据ThreadLoacal来存储和获取相应的value。

Android