Android 消息机制深入源码分析 [ 一 ]
Android 消息机制之 ThreadLocal 深入源码分析 [ 二 ]
Android 消息机制之 Looper 深入源码分析 [ 三 ]
Android 消息机制之 Message 与消息对象池的深入源码分析 [ 四 ]
Android 消息机制之 MessageQueue 深入源码分析 [ 五 ]
Android 消息机制之初识Handler [ 六 ]
Android 消息机制之 Handler 发送消息的深入源码分析 [ 七 ]
Android 消息机制之 MessageQueue.next() 消息取出的深入源码分析 [ 八 ]
Android 消息机制之消息的其他处理深入源码分析 [ 九 ]
Android 消息机制总结 [ 十 ]
接着上一章, 从本章开始源码进行分析 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.java190行),ThreadLocalMap中又有一个Entry类型叫table数组, 而Entry又是以键值对的形式来存储数据,key为ThreadLocal类型. 所以, 同一线程可以有多个ThreadLocal, 但是对于同一线程不同的ThreadLocal来说, 它们共享的同一个table数组, 只是在table中的索引不同. -
Entry的key是弱引用, 当空间不足的时候, 会清理未被引用的Entry对象. 所以会有过期的Entry, 也就是Entry不为空, 但是Entry.get() - 对于某一
ThreadLocal来说, 它的索引值是确定的, 在不同线程之间访问时, 访问的是不同的table数组的同一位置, 只不过这个不同线程之间的table是独立的.
