欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-14 15:12:11
...

概述

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。