Android关于ThreadLocal的思考和总结


    • 前言
      • Handler机制引出ThreadLocal
    • 分析
      • 案例展示及运行结果
      • ThreadLocal类结构预览
      • ThreadLocal探秘
      • ThreadLocal源码解读
      • ThreadLocal内存泄漏的问题
      • InheritableThreadLocal与ThreadLocal的区别
    • 总结
    • 参考
    • 其它

前言 Handler机制引出ThreadLocal
  • 关于ThreadLocal的分析,首先得从Android的消息机制谈起,可能我们最先想到的就是Android消息机制的上层接口Handler
  • 为了避免ANR,我们会通常把耗时操作放在子线程里面去执行,因为子线程不能更新UI,所以当子线程需要更新UI的时候就需要借助到Android的消息机制,也就是Handler机制了
关于Handler的原理,不是本文剖析的重点,这里仅给出一些相关结论,同时引出今天的主角ThreadLocal
  • Handler的处理过程运行在创建Handler的线程里
  • 一个Looper对应一个MessageQueue
  • 一个线程对应一个Looper
  • 一个Looper可以对应多个Handler
  • 线程是默认没有Looper的,线程需要通过Looper.prepare()、绑定Handler到Looper对象、Looper.loop()来建立消息循环
  • 主线程(UI线程),也就是ActivityThread,在被创建的时候就会初始化Looper,所以主线程中可以默认使用Handler
  • 可以通过Looper的quitSafely()或者quit()方法终结消息循环,quitSafely相比于quit方法安全之处在于清空消息之前会派发所有的非延迟消息。
  • 不确定当前线程时,更新UI时尽量调用post方法
如何保证一个线程对应一个Looper,同时各个线程之间的Looper互不干扰就引出了接下来要讨论的ThreadLocal
public final class Looper { private static final String TAG = "Looper"; // sThreadLocal.get() will return null unless you've called prepare(). static final ThreadLocal sThreadLocal = new ThreadLocal(); ....//省略 }

分析 案例展示及运行结果
这里先给出ThreadLocal和InheritableThreadLocal的简单实用demo
public class ThreadLocalTest { static final String CONSTANT_01 = "CONSTANT_01"; static final String CONSTANT_02 = "CONSTANT_02"; public static void main(String[] args) throws InterruptedException { ThreadLocal threadLocal = new ThreadLocal(); threadLocal.set(CONSTANT_01); InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal(); inheritableThreadLocal.set(CONSTANT_01); Thread thread_1 = new TestThread(threadLocal, inheritableThreadLocal); thread_1.setName("thread_01"); thread_1.start(); thread_1.join(); System.out.println("" + Thread.currentThread().getName() + "******************************************"); System.out.println("" + Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get()); System.out.println("" + Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get()); System.out.println("" + Thread.currentThread().getName() + "******************************************"); } }class TestThread extends Thread { ThreadLocal threadLocal; InheritableThreadLocal inheritableThreadLocal; public TestThread(ThreadLocal threadLocal, InheritableThreadLocal inheritableThreadLocal) { super(); this.threadLocal = threadLocal; this.inheritableThreadLocal = inheritableThreadLocal; }public void run() { System.out.println(Thread.currentThread().getName() + "******************************************"); System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get()); System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get()); System.out.println(Thread.currentThread().getName() + "******************************************\n"); threadLocal.set(ThreadLocalTest.CONSTANT_02); inheritableThreadLocal.set(ThreadLocalTest.CONSTANT_02); System.out.println(Thread.currentThread().getName() + "*************(Reset Value)****************"); System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get()); System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get()); System.out.println(Thread.currentThread().getName() + "*************(Reset Value)****************\n"); } }

运行结果:
thread_01****************************************** thread_01ThreadLocal: null thread_01InheritableThreadLocal: CONSTANT_01 thread_01******************************************thread_01*************(Reset Value)**************** thread_01ThreadLocal: CONSTANT_02 thread_01InheritableThreadLocal: CONSTANT_02 thread_01*************(Reset Value)****************main****************************************** mainThreadLocal: CONSTANT_01 mainInheritableThreadLocal: CONSTANT_01 main******************************************

如果这个时候你对运行结果有疑问 或者说 「我擦」怎么又突然冒出来一个InheritableThreadLocal,那么请继续往下看
ThreadLocal类结构预览
当然,我们肯定要先从ThreadLocal开始说起:
先从大体上看一下,可以发现,Java和Android中ThreadLocal的类结构(包括部分细节)还是有一些区别的,不过Android中的实现方式越来越贴近Java版
第一张图为jdk1.8.0_131中ThreadLocal的类结构:


Android关于ThreadLocal的思考和总结
文章图片


第二张图为android-25中ThreadLocal的类结构:

Android关于ThreadLocal的思考和总结
文章图片


ThreadLocal探秘
这里主要以Android-25(Android7.1.1)的源码为基础进行分析,其实几乎和Java版本的源码一致
首先澄清一下对ThreadLocal的错误认知:
  • ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
  • ThreadLocal的目的是为了解决多线程访问资源时的共享问题
为什么这么说那?
我们看看Android源码中是如何介绍ThreadLocal的:
This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
描述的大致意思是这样:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
可以这么总结:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
有时候大家会拿同步机制(如synchronized)和ThreadLocal做对比,怎么说才能不引起误解那?
可以这么理解:
对于多线程资源共享的问题,前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。但是ThreadLocal却并不是为了解决并发或者多线程资源共享而设计的
所以ThreadLocal既不是为了解决共享多线程的访问问题,更不是为了解决线程同步问题,ThreadLocal的设计初衷就是为了提供线程内部的局部变量,方便在本线程内随时随地的读取,并且与其他线程隔离。
ThreadLocal的应用场景:
  • 当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候
    如:属性动画为每个线程设置AnimationHandler、Android的Handler消息机制中通过ThreadLocal实现Looper在线程中的存取、EventBus获取当前线程的PostingThreadState对象或者即将被分发的事件队列或者当前线程是否正在进行事件分发的布尔值
  • 复杂逻辑下的对象传递
    使用参数传递的话:当函数调用栈更深时,设计会很糟糕,为每一个线程定义一个静态变量监听器,如果是多线程的话,一个线程就需要定义一个静态变量,无法扩展,这时候使用ThreadLocal就可以解决问题。
ThreadLocal源码解读
构造函数:
public ThreadLocal() { }

创建一个线程的本地变量
initialValue函数:
protected T initialValue() { return null; }

该函数在调用get函数的时候会第一次调用,但是如果一开始就调用了set函数,则该函数不会被调用。通常该函数只会被调用一次,除非手动调用了remove函数之后又调用get函数,这种情况下,get函数中还是会调用initialValue函数。该函数是protected类型的,很显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重载,以指定初始值,比如
public class TestThreadLocal { private static final ThreadLocal value = https://www.it610.com/article/new ThreadLocal() { @Override protected Integer initialValue() { return Integer.valueOf(1); } }; }

get函数:
该函数用来获取与当前线程关联的ThreadLocal的值,如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回
public T get() { //1、首先获取当前线程 Thread t = Thread.currentThread(); //2、根据当前线程获取一个map ThreadLocalMap map = getMap(t); //3、如果获取的map不为空,则在map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到5 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); //4、如果e不为null,则返回e.value,否则转到5 if (e != null) return (T)e.value; } //5、map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的map return setInitialValue(); }ThreadLocalMap getMap(Thread t) { return t.threadLocals; }private T setInitialValue() { T value = https://www.it610.com/article/initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

值得注意的是,上面getMap方法中获取的threadLocals即是Thread中的一个成员变量
public class Thread implements Runnable { ...//省略 /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is maintained by the InheritableThreadLocal class.*/ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; ...//省略 }

这里的inheritableThreadLocals会在下文分析InheritableThreadLocal涉及到
set函数:
set函数用来设置当前线程的该ThreadLocal的值,设置当前线程的ThreadLocal的值为value
public void set(T value) { //1、首先获取当前线程 Thread t = Thread.currentThread(); //2、根据当前线程获取一个map ThreadLocalMap map = getMap(t); if (map != null) //3、map不为空,则把键值对保存到map中 map.set(this, value); //4、如果map为空(第一次调用的时候map值为null),则去创建一个ThreadLocalMap对象并赋值给map,并把键值对保存到map中。 else createMap(t, value); }

remove函数:
remove函数用来将当前线程的ThreadLocal绑定的值删除,在某些情况下需要手动调用该函数,防止内存泄露。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }

ThreadLocalMap
可以看成一个HashMap,但是它本身具体的实现却与java.util.Map沾不上一点关系。只是内部的实现跟HashMap类似(通过哈希表的方式存储)。
static class ThreadLocalMap { static class Entry extend WeakReference { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = https://www.it610.com/article/v; } } ...//省略 }

大致类结构如下图所示:


Android关于ThreadLocal的思考和总结
文章图片


ThreadLocalMap中定义了Entry数组实例table,用于存储Entry。相当于使用一个数组维护一张哈希表,负载因子是最大容量的2/3
private Entry[] table;

关于ThreadLocalMap重要函数的分析会结合下一节ThreadLocal内存泄漏的问题一并讨论
PS:Android早期版本,这部分的数据结构是通过Values实现的,Values中也有一个table的成员变量,table是一个Object数组,也是以类似map的方式来存储的。偶数单元存储的是key,key的下一个单元存储的是对应的value,所以每存储一个元素,需要两个单元,所以容量一定是2的倍数。这里的key存储的也是ThreadLocal实例的弱引用
ThreadLocal内存泄漏的问题
ThreadLocal里面使用了一个存在弱引用的map,当释放掉ThreadLocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用ThreadLocal的remove方法.
在ThreadLocal的生命周期中,都存在这些引用.
看下图(来源参考): 实线代表强引用,虚线代表弱引用.


Android关于ThreadLocal的思考和总结
文章图片


  • 每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个ThreadLocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向ThreadLocal. 当把ThreadLocal实例置为null以后,没有任何强引用指向ThreadLocal实例,所以ThreadLocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
  • 所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在ThreadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
  • 为了最小化减少内存泄露的可能性和影响,(设计中加上了一些防护措施)在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。
getEntry函数:
首先从ThreadLocal的直接索引位置获取Entry e,如果e不为null并且key相同则返回e;如果e为null或者key不一致则通过getEntryAfterMiss向下一个位置查询
private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }

getEntryAfterMiss函数:
这个过程中遇到的key为null的Entry都会被擦除(Entry内的value也就没有强引用链,自然会被回收)
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal k = e.get(); if (k == key) //命中 return e; if (k == null) //如果key值为null,则擦除该位置的Entry expungeStaleEntry(i); else i = nextIndex(i, len); //继续向下一个位置查询 e = tab[i]; } return null; }

set函数:
set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露
private void set(ThreadLocal key, Object value) {Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal k = e.get(); if (k == key) { e.value = https://www.it610.com/article/value; return; }if (k == null) { replaceStaleEntry(key, value, i); return; } }tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz>= threshold) rehash(); }

小结:
  • 虽然源码中对内存泄漏做了很好的防护作用,但是很多情况下还是需要手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。
  • 所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
InheritableThreadLocal与ThreadLocal的区别
InheritableThreadLocal比ThreadLocal多一个特性,继承性,可以从父线程中得到初始值
首先浏览下 InheritableThreadLocal 类中有什么东西:
public class InheritableThreadLocal extends ThreadLocal { protected T childValue(T parentValue) { return parentValue; } ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }

其实就是重写了3个方法
  • InheritableThreadLocal的get()方法会调用getMap(t),而这时返回的是inheritableThreadLocals(Thread的一个成员变量)
  • 父线程往子线程中传递值是在Thread thread = new Thread()的时候,然后调用线程内部的init方法进行处理,最终就是不断的把当前线程的inheritableThreadLocals值复制到我们新创建的线程中的inheritableThreadLocals 中
  • 主要面对的是线程中再创建线程的场景,类似开篇举的例子,而对于子线程之间的传递或者线程池中得到父线程的值则不可行(这部分没有深入研究)
总结 现在回过头来分析开篇的例子:
  • 第一次打印:子线程中的ThreadLocal没有赋值,所以为null,而子线程中的InheritableThreadLocal却可以获取到父线程中的值CONSTANT_01
  • 第二次打印:子线程ThreadLocal和InheritableThreadLocal同时重新赋值CONSTANT_02,所以打印出的结果都为CONSTANT_02
  • 第三次打印:回到主线程,主线程和子线程都是维护自己的副本,所以子线程赋值CONSTANT_02并不会对主线程有任何影响,所以主线程打印出的结果依旧都是CONSTANT_01
参考 https://www.zhihu.com/question/23089780
https://github.com/pzxwhc/MineKnowContainer/issues/12
https://github.com/pzxwhc/MineKnowContainer/issues/20
http://blog.csdn.net/singwhatiwanna/article/details/48350919
http://www.cnblogs.com/onlywujun/p/3524675.html
其它
  • 我的CSDN博客地址:http://blog.csdn.net/s003603u
  • 我的GitHub地址:https://github.com/soulrelay
  • 我的简书地址:http://www.jianshu.com/u/514ca03bbc17
  • 我的掘金地址:https://juejin.im/user/56f3d9d1816dfa00522b8f20
  • 我的个人站点: http://sushuai.tech/


Android关于ThreadLocal的思考和总结
文章图片
【Android关于ThreadLocal的思考和总结】

    推荐阅读