zoukankan      html  css  js  c++  java
  • Android 异步消息处理机制前篇(一):深入理解ThreadLocal

    版权声明:本文出自汪磊的博客,转载请务必注明出处。

    ThreadLocal简介

    ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其它线程来说无法获取到数据。比如:我们在子线程A中存储数据x,只有在子线程中取数据才会获取存储的数据x,在B线程中获取是获取不到的。存储与获取数据都是要在同一线程中操作。

    关于ThreadLocal的使用这里就不举例了(PS:其实是TMD简单了),我们直接分析存储,获取数据核心源码

    安卓中ThreadLocal存储数据核心源码分析

    ThreadLocal中存储数据调用的是set(T value)方法(T是什么?泛型),源码如下:

    1 public void set(T value) {
    2         Thread currentThread = Thread.currentThread();
    3         Values values = values(currentThread);
    4         if (values == null) {
    5             values = initializeValues(currentThread);
    6         }
    7         values.put(this, value);
    8 }

    第2行:获取当前线程currentThread,如果在主线程调用set方法,则为主线程,如果在子线程调用则为对应的子线程。

    第3行:调用values方法获取Values对象values(PS:这句话挺拗口),看下values方法:

    1  Values values(Thread current) {
    2         return current.localValues;
    3 }

    很简单,就是返回第2行代码获取的currentThread中的localValues,Thread类中定义了一个ThreadLocal.Values类型的变量localValues。

    回到set(T value)方法继续看4-6行:如果values为null则对其进行初始化,第一次调用set方法的时候这里肯定为空,我们看下初始化initializeValues方法:

    1 Values initializeValues(Thread current) {
    2         return current.localValues = new Values();
    3  }

    同样很简单,就是对我们获取的当前线程中localValues变量进行初始化。

    第7行:调用values的put方法进行数据的存储。

    Values是什么鬼呢?怎么最终调用的是Values的put方法?其实Values是ThreadLocal的内部类,内部维护一个Object类型的数组table,我们存储的数据都是存储在内部数组中的,同样获取数据也是调用的Values类中相应方法。 接下来我们分析一下Values类。

    ThreadLocal中内部类Values源码分析

    先从构造方法看看吧:

     1 //数组初始化大小
     2 private static final int INITIAL_SIZE = 16;
     3 //存储数据的数组
     4 private Object[] table;
     5 //用于计算存储数据的下标index,长度为table.length - 1,可以防止角标越界
     6 private int mask;
     7 //记录数组中数据数量
     8 private int size;
     9 
    10 Values() {
    11           initializeTable(INITIAL_SIZE);
    12             this.size = 0;
    13             this.tombstones = 0;
    14  }
    15 
    16 private void initializeTable(int capacity) {
    17             this.table = new Object[capacity * 2];
    18             this.mask = table.length - 1;
    19             this.clean = 0;
    20             this.maximumLoad = capacity * 2 / 3; // 2/3
    21  }

    这里就没什么要说的了,就是一些初始化方法。接下来我们看下存储数据的方法put(ThreadLocal<?> key, Object value)源码:

     1 void put(ThreadLocal<?> key, Object value) {
     2             cleanUp();
     3 
     4             // Keep track of first tombstone. That's where we want to go back
     5             // and add an entry if necessary.
     6             int firstTombstone = -1;
     7 
     8             for (int index = key.hash & mask;; index = next(index)) {
     9                 Object k = table[index];
    10 
    11                 if (k == key.reference) {
    12                     // Replace existing entry.
    13                     table[index + 1] = value;
    14                     return;
    15                 }
    16 
    17                 if (k == null) {
    18                     if (firstTombstone == -1) {
    19                         // Fill in null slot.
    20                         table[index] = key.reference;
    21                         table[index + 1] = value;
    22                         size++;
    23                         return;
    24                     }
    25 
    26                     // Go back and replace first tombstone.
    27                     table[firstTombstone] = key.reference;
    28                     table[firstTombstone + 1] = value;
    29                     tombstones--;
    30                     size++;
    31                     return;
    32                 }
    33 
    34                 // Remember first tombstone.
    35                 if (firstTombstone == -1 && k == TOMBSTONE) {
    36                     firstTombstone = index;
    37                 }
    38             }
    39  }

    第6行:firstTombstone 用于记录已经删除的table数组中数据角标,严格说是key的角标,如果有被删除的数据则存储数据的时候在当前位置直接插入即可,节省内存啊。别急,分析完你会理解的。

    8-38行遍历table数组中已经存储的数据与将要存储的数据比较,不同情况进行不同的处理(PS:这尼玛不是废话吗)

    第8行:int index = key.hash & mask 算出存储数据key的索引,key就是当前的ThreadLocal,还记得这个mask吗?初始化时设置长度为table.length - 1,进行按位与操作计算出的index还可以保障角标不会越界。然后每次循环后index = next(index),next源码如下:

    1 private int next(int index) {
    2             return (index + 2) & mask;
    3 }

    看这里就会明白,index索引每次循环都会加2,说明不是挨个循环table数组中元素,而是0,2,4,6.。。。这种跳跃式循环,为什么这样呢?别急,分析完整个方法你会明白(PS:总告诉我别急,我都快急死了)

    第9行:从table数组中取出对应索引的元素

    第11-15行:取出的元素key与要存储的key的reference比较,如果相等则在下一个位置index+1,存储value值,然后return整个方法执行完毕。reference又是什么鬼?其实就是当前ThreadLocal的弱引用:

    1  private final Reference<ThreadLocal<T>> reference = new WeakReference<ThreadLocal<T>>(this);

    也是为了内存考虑的。

    11-15行主要判断要存储的数据之前是否存储过,如果存储过则覆盖存储。

    17-32行:17行表明之前没有存储过要存储的数据,则进入判断。

    18-24行:首先判断firstTombstone是否为-1,firstTombstone默认值-1,上面说了firstTombstone 用于记录已经删除的table数组中数据角标,如果等于-1,则表明没找到已经删除数据的索引。

    20-23行:将要存储的数据依次存储在index和index+1的位置上。然后return整个方法。

    27-31行:经过上面的分析这里就很好理解了,就是找到有删除的元素,并且firstTombstone就是被删除元素的key的索引,则将要存储数据的key.reference以及value依次存储在firstTombstone与firstTombstone+1位置上,也就是覆盖存储,覆盖已经被删除数据的位置。

    35-37行:就是寻找被删除数据角标的过程,其中TOMBSTONE用于记录被删除的数据。

    好了,到此整个存储过程就分析完了,是不是还有点蒙蔽,到底怎么存储的呢?上图(是时候展示我强大的画图能力了):

    看到了吧,其实存储就是在数组相邻位置依次存储key.reference与value。看到这里上面的所有疑问就都解开了,为什么循环遍历的时候index是每次加2的?因为key的存储是隔一个元素存储的,key.reference与value才是一个数据整体。好了具体存储想说的就都说完了。

    我们回头想一个问题?ThreadLocal是怎么将数据只存储在当前线程并且只有当前线程可以获取?其实只要仔细想想文章一开始就提到的ThreadLocal中set方法就不难理解了。

    ThreadLocal中set方法一开始就获取当前线程,然后从当前线程获取Values对象,如果当前线程没有则给当前线程初始化一个Values对象,最终调用Values对象的put方法存储数据,获取数据也是同样的逻辑。重点就是无论存储还是获取数据都是调用当前线程中的Values对象的存储或者获取数据的方法,重点是当前线程的Values对象,这个对象只存储在当前线程,其余线程也存在Values对象,但是不同线程是不一样的,不同线程维护不同的Values对象,这就是核心所在。

    至于ThreadLocal的get方法我就不继续分析了,最核心的部分上面已经分析完了,感兴趣的可以自己试着去分析一下。

    为什么存储的key是当前ThreadLocal的弱引用?

    试想一下如果我们存储的是ThreadLocal的强引用,我们将ThreadLocal对象置为null,此时ThreadLocal对应的内存会被顺利回收吗?显然不会,虽然ThreadLocal对象被置为null了,但是Values对象的数组中依然存储着ThreadLocal的强引用,对应内存不能顺利被回收,这就必然造成内存泄漏。

    如果存储的是ThreadLocal的弱引用,那么Values对象table数组中对应key则可以被顺利收回,key对应的value在下一次调用的时候会被清除。

    这里是不是又会出现一个问题?

    由于Values和对应Thread生命周期是一样的,如果我们将ThreadLocal对象置为null,那么table中对应的key就可能被GC回收,当时此时value值还没有回收啊,是不是很别扭,这就要求我们写代码的时候要规范,每次使用完ThreadLocal都调用remove()方法,清除数据。

    如下:

     1 ThreadLocal<String> t1 = new ThreadLocal<String>() {
     2 
     3             @Override
     4             protected String initialValue() {
     5                 //
     6                 return "adc";
     7             }
     8 };
     9         
    10 Toast.makeText(MainActivity.this, t1.get(), 0).show();
    11         
    12 t1.remove();
    13 t1 = null;

    好了,本文到此结束,希望对你有帮助,废话就不多说了,学到的才是自己的。

    下一篇分析Message.obtain()所说的消息池到底是什么玩意?怎么实现的?你真正的理解吗?

  • 相关阅读:
    拯救者R720安装Ubuntu之后无法连接无线网络
    centos源码安装mysql5.7.25-boost
    mysql的报错
    Nginx配置文档
    centos6.5删除/boot后恢复
    Spring Boot 初体验(11)添加JSP支持
    Spring Boot 初体验(10)使用使用freemarker
    Spring Boot 初体验(9)使用thymeleaf
    Spring Boot 初体验(8)配置server信息
    Spring Boot 初体验(7)全局异常捕捉
  • 原文地址:https://www.cnblogs.com/leipDao/p/7831917.html
Copyright © 2011-2022 走看看