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

概述

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。

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

推荐阅读更多精彩内容