Android关于ThreadLocal的思考和总结

前言

Handler机制引出ThreadLocal

  • 关于ThreadLocal的分析,首先得从Android的消息机制谈起,可能我们最先想到的就是Android消息机制的上层接口Handler
  • 为了避免ANR,我们会通常把耗时操作放在子线程里面去执行,因为子线程不能更新UI,所以当子线程需要更新UI的时候就需要借助到Android的消息机制,也就是Handler机制了

关于Handler的原理,不是本文剖析的重点,这里仅给出一些相关结论,同时引出今天的主角ThreadLocal

  • 如果handler绑定的是当前线程的looper,那么处理过程也是运行在当前线程(主线程使用默认构造函数创建的Handler默认绑定的是主线程的looper)
  • 一个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<Looper> sThreadLocal = new ThreadLocal<Looper>();
    
    ....//省略
    }

分析

案例展示及运行结果

这里先给出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<String> threadLocal = new ThreadLocal<String>();
        threadLocal.set(CONSTANT_01);

        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>();
        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<String> threadLocal;
    InheritableThreadLocal<String> inheritableThreadLocal;

    public TestThread(ThreadLocal<String> threadLocal, InheritableThreadLocal<String> 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_01   ThreadLocal: null
thread_01   InheritableThreadLocal: CONSTANT_01
thread_01******************************************

thread_01*************(Reset Value)****************
thread_01   ThreadLocal: CONSTANT_02
thread_01   InheritableThreadLocal: CONSTANT_02
thread_01*************(Reset Value)****************

   main  ******************************************
   main     ThreadLocal: CONSTANT_01
   main     InheritableThreadLocal: CONSTANT_01
   main  ******************************************

如果这个时候你对运行结果有疑问 或者说 「我擦」怎么又突然冒出来一个InheritableThreadLocal,那么请继续往下看

ThreadLocal类结构预览

当然,我们肯定要先从ThreadLocal开始说起:

先从大体上看一下,可以发现,Java和Android中ThreadLocal的类结构(包括部分细节)还是有一些区别的,不过Android中的实现方式越来越贴近Java版

第一张图为jdk1.8.0_131中ThreadLocal的类结构:


第二张图为android-25中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
<tt>get</tt> or <tt>set</tt> method) has its own, independently initialized copy of the variable. <tt>ThreadLocal</tt> 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<Integer> value = new ThreadLocal<Integer>() {
        @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 = 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<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        ...//省略
 }

大致类结构如下图所示:

这里写图片描述

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的生命周期中,都存在这些引用.

看下图(来源参考): 实线代表强引用,虚线代表弱引用.

这里写图片描述
  • 每个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 = 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<T> extends ThreadLocal<T> {
    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

其它

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容