接着上一章, 从本章开始源码进行分析 Android 的消息机制.
1. ThreadLocal 是什么.
- ThreadLocal 是一个线程内部的数据存储类, 通过它可以在指定的线程中存储数据, 数据存储以后, 只有在指定线程中可以获取到存储的数据, 对于其他线程来说则无法获取到数据.
2. 什么情况下使用 ThreadLocal
-
当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候, 就可以考虑使用 ThreadLocal, 比如对于 Handler 来说, 它需要获取当前线程的 Looper, 很显然 Looper 的作用域就是线程并且不同线程具有不同的 Looper, 这个时候通过 ThreadLocal 就可以轻松实现 Looper 在线程中的获取.
-
另一个场景是在复杂逻辑下的对象传递, 比如监听器的传递, 有时候一个线程中的任务过于繁杂, 这可能表现为函数调用栈比较深以及代码入口的多样性. 在这种情况下, 又需要监听器能够贯穿整个线程的执行过程, 这个时候就可以采用 ThreadLocal. 采用 ThreadLocal 可以让监听器作为线程内的全局对象而存在, 在线程内只要通过 get 方法就可以获取到监听器. 如果不采用 ThreadLocal, 那么可能会有下面两种方式.
- 将监听器通过参数的形式在函数调用栈中进行传递, 但是当函数调用栈很深的时候, 通过函数传递监听器对象几乎是不可能接受的, 会让程序看起来很 Low.
- 将监听器作为静态变量供线程访问. 这个倒是可以接受, 但是这种情况是不具有可扩展性的. 比如两个线程在执行, 那么就需要提供两个静态的监听器对象, 如果有 10 个线程在并发执行呢, 100 个呢 ?
- 而采用 ThreadLocal, 每个监听器对象都在自己的线程内部存储, 根本不会有第二种情况出现.
下面使用一个例子来简单的使用一下 ThreadLocal.
3. 例
public class MainActivity extends AppCompatActivity {
final String TAG = "MainActivity";
private ThreadLocal<Boolean> mBooleanThreadLocal = new ThreadLocal<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
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() {
// mBooleanThreadLocal.set(false);
Log.d(TAG,"[ Thread#2 ] mBooleanThreadLocal =" + mBooleanThreadLocal.get());
}
}.start();
}
}
输出结果
D/MainActivity: [ Thread#main ] mBooleanThreadLocal =true
D/MainActivity: [ Thread#1 ] mBooleanThreadLocal =false
D/MainActivity: [ Thread#2 ] mBooleanThreadLocal =null
- 在主线程中设置 mBooleanThreadLocal 为 true, 在子线程 Thread#1 中设置 mBooleanThreadLocal 为 false. 在子线程 Thread#2 不设置. 然后在三个线程中分别通过 get 方法获取 mBooleanThreadLocal 的值, 根据上面对 ThreadLocal 的描述, 这个时候, 主线程中应该是 true,子1中应该是 false, 子2中没有设置应该是 null.
- 虽然他们在不同的线程访问的是同一个 ThreadLocal 对象, 但是获取到的值却是不同的. 不同线程访问 ThreadLocal 的 get 方法, ThreadLocal 内部会从各自线程中取出一个数组, 然后再从数组中根据当前线程 ThreadLocal 的索引去查找对应的 value 值, 显然, 不同线程中的数据是不同的, 在哪个线程中设置的值, 那么这个值就存储在哪个线程中. 这就是为什么通过 ThreadLocal 可以在不同线程中维护一套数据副本, 并且彼此互不干扰
4. ThreadLocal 内部实现
关键代码如下:
class ThreadLocal<T>{
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
//自增
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//分析 1
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//分析 2
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
ThreadLocalMap
是ThreadLocal
的一个静态内部类, 这里先不解释, 下面会有说到.
- 分析 1
根据当前所在线程
getMap(t)
获取ThreadLocalMap
对象, 如果ThreadLocalMap != null
就将需要保存的值以<key, value>
的形式保存,key
是ThreadLocal
实例,value
是传入的参数value
. 如果ThreadLocalMap == null
就创建一个ThreadLocalMap
对象, 并将这个对象赋值给当前线程的threadLocals
属性, 并将值保存.
- 分析 2
也是根据当前所在线程获取到
ThreadLocalMap
对象, 然后进行判断. 再根据当前ThreadLocal
作为 key 进行取值并返回, 如果ThreadLocalMap == null
那么会调用setInitialValue()
方法. 后面会说到这个方法.
5. ThreadLocalMap 的内部实现
关键代码 ThreadLocal.java 298行
class ThreadLocal{
...
static class ThreadLocalMap{
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private void set(ThreadLocal<?> key, Object value) {
//一起贴出来代码会过长, 不方便阅读, 所以下面会单独拿出来看.
}
private Entry getEntry(ThreadLocal<?> key) {
//一起贴出来代码会过长, 不方便阅读, 所以下面会单独拿出来看.
}
}
}
Entry
以键值对存储,Key
就为 一个ThreadLocal
对象,value
是一个Object
对象.
ThreadLocalMap
是ThreadLocal
中的一个静态内部类, 构造方法中定了一个初始大小为 16 的Entry
数组实例table
, 用于存储Entry
对象. 那么不难理解key, value
就是被封装到了Entry
对象里. 也就是说ThreadLocalMap
中维护着一张哈希表, 也就是table
数组, 并设定了一个临界值.setThreshold
, 但是当哈希列存储的对象达到容量的 2/3 的时候, 就会扩容.
ThreadLocal
中调用的get(), set()
其实就是调用ThreadLocalMap
中的set(), getEntry()
这两个方法.
5.1 现在开始看 ThreadLocalMap.set()
class ThreadLocal{
...
static class ThreadLocalMap{
...
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//分析 1
int index = key.threadLocalHashCode & (len-1);
for (Entry e = tab[index]; e != null; e = tab[index = nextIndex(index, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, index);
return;
}
}
//分析 2
tab[index] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(index, sz) && sz >= threshold){
rehash();
}
}
//分析 3
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4){
resize();
}
}
}
}
- 分析 1
在 5 中得知
table
是一个Entry
数组, 根据数组的长度与当前线程ThreadLocal
对象的哈希值, 先计算要存储的位置, 然后判断指定的位置是否有数据, 有数据就开始遍历Entry
, 在循环中获取指定位置Entry
中的ThreadLocal
是否与传入的ThreadLocal
相同. 如果相同, 就替换指定位置Entry
的value
为传入的value
值. 并返回. 如果没有获取到ThreadLocal
, 说明出现了过期数据, 需要遍历清洗后并在指定位置插入新的数据, 其他的数据后移. 然后返回.
- 分析 2
如果 table 数组中不存在指定位置的
Entry
对象, 那么就创建一个Entry
对象存储. 并且判断table
数组中存储对象的个数是否超过了临界值, 如果超过了, 就调用rehash()
方法进行扩容和重新计算所有对象的位置.
- 分析 3
先是调用
expungeStaleEntries()
方法删除过期的Entry
对象 (怎么判断过期呢, 就是Entry
不为空, 但是Entry.get()
获取的key
为空.). 如果清理完后,size >= threshold - threshold / 4
成立 则扩容两倍.
- set 总结
- 若指定位置的
index
已有数据Entry
对象, 逐个遍历Entry
- 若
index
处key
相同, 折更新value
.- 若
index
处key
为null
, 则调用replaceStaleEntry ()
方法清理过期数据并插入新数据. (从index
处挨个遍历, 直到找到相同key
并更新value
结束, 一直未找到, 则在index
处放入新的Entry
对象.)replaceStaleEntry ()
遍历时会将Entry
逐个后移, 也就是说我们set
进去最新的Entry
一定会放在index
处, 方便get
时直接命中.index
处无数据, 则放入新的Entry
, 税后清理过期数据并判断是否需要 扩容 2 倍.
6. ThreadLocal 总结
- 每个线程都持有一个
ThreadLocalMap
的引用 (代码在Thread.java
190行),ThreadLocalMap
中又有一个Entry
类型叫table
数组, 而Entry
又是以键值对的形式来存储数据,key
为ThreadLocal
类型. 所以, 同一线程可以有多个ThreadLocal
, 但是对于同一线程不同的ThreadLocal
来说, 它们共享的同一个table
数组, 只是在table
中的索引不同. -
Entry
的key
是弱引用, 当空间不足的时候, 会清理未被引用的Entry
对象. 所以会有过期的Entry
, 也就是Entry
不为空, 但是Entry.get()
- 对于某一
ThreadLocal
来说, 它的索引值是确定的, 在不同线程之间访问时, 访问的是不同的table
数组的同一位置, 只不过这个不同线程之间的table
是独立的.
网友评论