Looper
中有一个特殊的概念,那就是ThreadLocal
,ThreadLocal
并不是线程,它的作用是可以在每个线程中存储数据。大家知道,Handler
创建的时候会采用当前线程的Looper
来构造消息循环系统,那么Handler
内部如何获取到当前线程的Looper
呢?这就要使用ThreadLocal
了,ThreadLocal
可以在不同的线程之中互不干扰地存储并提供数据。
ThreadLocal
是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其它线程来说无法获取到数据。在日常开发中用到ThreadLocal
的地方较少,但是在某些特殊的场景下,通过ThreadLocal
可以轻松地实现一些看起来很复杂的功能,这一点在Android的源码中也有所体现,比如Looper
、ActivityThread
以及AMS
中都用到了ThreadLocal
。具体到ThreadLocal
的使用场景,这个不好统一地来描述,一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal
。比如对于Handler
来说,它需要获取当前线程的Looper
,很显然Looper
的作用域就是线程并且不同线程具有不同的Looper
,这个时候通过ThreadLocal
就可以轻松实现Looper
在线程中的存取,如果不采用ThreadLocal
,那么系统就必须提供一个全局的哈希表供Handler
查找指定线程的Looper
,这样一来就必须提供一个类似于LooperManager
的类了,但是系统并没有这么做而是选择了ThreadLocal
,这就是ThreadLocal
的好处。
ThreadLocal
另一个使用场景是复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,在这种情况下,我们又需要监听器能够贯穿整个线程的执行过程,这个时候可以怎么做呢?其实就可以采用ThreadLocal
,采用ThreadLocal
可以让监听器作为线程内的全局对象而存在,在线程内部只要通过get
方法就可以获取到监听器。
介绍了那么多ThreadLocal
的知识,可能还是有点抽象,下面通过实际的例子为大家演示ThreadLocal
的真正含义。首先定义一个ThreadLocal
对象,这里选择Boolean
类型的,如下所示:
private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<Boolean>();
然后分别在主线程
、子线程1
和子线程2
中设置和访问它的值,代码如下所示:
mBooleanThreadLocal.set(true);
Log.d(TAG, "[Thread#main]mBooleanThreadLocal=" + mBooleanThreadLocal.get());
new Thread("Thread#1") {
@Override
public void run() {
mBooleanThreadLocal.set(false);
Log.d(TAG, "[Thread#1]mBooleanThreadLocal="
+ mBooleanThreadLocal.get());
};
}.start();
new Thread("Thread#2") {
@Override
public void run() {
Log.d(TAG, "[Thread#2]mBooleanThreadLocal="
+ mBooleanThreadLocal.get());
};
}.start();
在上面的代码中,在主线程
中设置mBooleanThreadLocal
的值为true
,在子线程1
中设置mBooleanThreadLocal
的值为false
,在子线程2
中不设置mBooleanThreadLocal
的值,然后分别在3个线程中通过get
方法去mBooleanThreadLocal
的值,根据前面对ThreadLocal
的描述,这个时候,主线程
中应该是true
,子线程1
中应该是false
,而子线程2
中由于没有设置值,所以应该是null
,安装并运行程序,日志如下所示:
D/TestActivity(8676):[Thread#main]mBooleanThreadLocal=true
D/TestActivity(8676):[Thread#1]mBooleanThreadLocal=false
D/TestActivity(8676):[Thread#2]mBooleanThreadLocal=null
从上面日志可以看出,虽然在不同线程中访问的是同一个ThreadLocal
对象,但是它们通过ThreadLocal
来获取到的值却是不一样的,这就是ThreadLocal
的奇妙之处。结合这这个例子然后再看一遍前面对ThreadLocal
的两个使用场景的理论分析,大家应该就能比较好地理解ThreadLocal
的使用方法了。ThreadLocal
之所以有这么奇妙的效果,是因为不同线程访问同一个ThreadLocal
的get
方法,ThreadLocal
内部会从各自的线程中取出一个数组,然后再从数组中根据当前ThreadLocal
的索引去查找出对应的value
值,很显然,不同线程中的数组是不同的,这就是为什么通过ThreadLocal
可以在不同的线程中维护一套数据的副本并且彼此互不干扰。
对ThreadLocal
的使用方法和工作过程做了一个介绍后,下面分析下ThreadLocal
的内部实现, ThreadLocal
是一个泛型类,它的定义为public class ThreadLocal<T>
,只要弄清楚ThreadLocal
的get
和set
方法就可以明白它的工作原理。
首先看ThreadLocal
的set
方法,如下所示:
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
在上面的set
方法中,首先会通过values
方法来获取当前线程中的ThreadLocal
数据,如果获取呢?其实获取的方式也是很简单的,在Thread
类的内容有一个成员专门用于存储线程的ThreadLocal
的数据,如下所示:ThreadLocal.Values localValues
,因此获取当前线程的ThreadLocal
数据就变得异常简单了。如果localValues
的值为null
,那么就需要对其进行初始化,初始化后再将ThreadLocal
的值进行存储。下面看下ThreadLocal
的值到底是怎么localValues
中进行存储的。在localValues
内部有一个数组:private Object[] table
,ThreadLocal
的值就是存在在这个table
数组中,下面看下localValues
是如何使用put
方法将ThreadLocal
的值存储到table
数组中的,如下所示:
void put(ThreadLocal<?> key, Object value) {
cleanUp();
// Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1;
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
}
if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
}
// Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;
return;
}
// Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}
上面的代码实现数据的存储过程,这里不去分析它的具体算法,但是我们可以得出一个存储规则,那就是ThreadLocal
的值在table数组中的存储位置总是为ThreadLocal
的reference
字段所标识的对象的下一个位置,比如ThreadLocal
的reference
对象在table
数组的索引为index
,那么ThreadLocal
的值在table
数组中的索引就是index+1
。最终ThreadLocal
的值将会被存储在table
数组中:table[index + 1] = value。
上面分析了ThreadLocal
的set
方法,这里分析下它的get
方法,如下所示:
public T get() {
// Optimized for the fast path.
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1];
}
} else {
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this);
}
可以发现,ThreadLocal
的get
方法的逻辑也比较清晰,它同样是取出当前线程的localValues
对象,如果这个对象为null
那么就返回初始值,初始值由ThreadLocal
的initialValue
方法来描述,默认情况下为null
,当然也可以重写这个方法。
如果localValues
对象不为null
,那就取出它的table
数组并找出ThreadLocal
的reference
对象在table
数组中的位置,然后table
数组中的下一个位置所存储的数据就是ThreadLocal
的值。
从ThreadLocal
的set
和get
方法可以看出,它们所操作的对象都是当前线程的localValues
对象的table
数组,因此在不同线程中访问同一个ThreadLocal
的set
和get
方法,它们对ThreadLocal
所做的读写操作仅限于各自线程的内部,这就是为什么ThreadLocal
可以在多个线程中互不干扰地存储和修改数据,理解ThreadLocal
的实现方式有助于理解Looper
的工作原理。
代码分析基于L
来张L上的ThreadLocal图:
在Android7.0之后ThreadLocal的实现跟JDK7上保持了一致,来张JDK7上的ThreadLocal图吧
个人感觉这样的修改,跟那个WeakReference导致的GC有关,以后再来求证下吧。[TODO]
参考文章:
https://blog.csdn.net/singwhatiwanna/article/details/48350919