Android 消息机制之 ThreadLocal 深入源码分析 [ 二 ]

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 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{
  
  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);
  }
}

ThreadLocalMapThreadLocal 的一个静态内部类, 这里先不解释, 下面会有说到.

  • 分析 1

根据当前所在线程 getMap(t) 获取 ThreadLocalMap 对象, 如果 ThreadLocalMap != null 就将需要保存的值以 的形式保存, keyThreadLocal 实例, 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> {
      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 对象.

ThreadLocalMapThreadLocal 中的一个静态内部类, 构造方法中定了一个初始大小为 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 相同. 如果相同, 就替换指定位置 Entryvalue 为传入的 value 值. 并返回. 如果没有获取到 ThreadLocal , 说明出现了过期数据, 需要遍历清洗后并在指定位置插入新的数据, 其他的数据后移. 然后返回.

  • 分析 2

如果 table 数组中不存在指定位置的 Entry 对象, 那么就创建一个 Entry 对象存储. 并且判断 table 数组中存储对象的个数是否超过了临界值, 如果超过了, 就调用 rehash() 方法进行扩容和重新计算所有对象的位置.

  • 分析 3

先是调用 expungeStaleEntries() 方法删除过期的 Entry 对象 (怎么判断过期呢, 就是 Entry 不为空, 但是 Entry.get() 获取的 key 为空.). 如果清理完后, size >= threshold - threshold / 4 成立 则扩容两倍.

  • set 总结
  • 若指定位置的 index 已有数据 Entry 对象, 逐个遍历 Entry
    • indexkey 相同, 折更新 value.
    • indexkeynull, 则调用 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 又是以键值对的形式来存储数据, keyThreadLocal 类型. 所以, 同一线程可以有多个 ThreadLocal, 但是对于同一线程不同的 ThreadLocal 来说, 它们共享的同一个 table 数组, 只是在 table 中的索引不同.
  • Entrykey 是弱引用, 当空间不足的时候, 会清理未被引用的 Entry 对象. 所以会有过期的 Entry, 也就是 Entry 不为空, 但是 Entry.get()
  • 对于某一 ThreadLocal 来说, 它的索引值是确定的, 在不同线程之间访问时, 访问的是不同的 table 数组的同一位置, 只不过这个不同线程之间的 table 是独立的.

你可能感兴趣的:(Android 消息机制之 ThreadLocal 深入源码分析 [ 二 ])