Java ThreadLocal 类的知识点解读

作者: 亦枫 | 来源:发表于2017-10-12 16:57 被阅读122次

    说起 Java 中的 ThreadLocal 类,可能很多安卓开发人员并不是很熟悉,毕竟很少有使用到的地方。但是如果你仔细分析过 Handler 源码的话,就一定见过这个类的出现。而 Handler 机制又是安卓知识体系中非常重要的一环,所以我们有必要了解一下 ThreadLocal 类的相关知识点。

    ThreadLocal 使用简介


    顾名思义,ThreadLocal 一定与线程有关,而事实也是如此。ThreadLocal<T> 作为一个泛型类,解决的是线程内部对象访问的问题,一定程度上避免对象作为参数到处传递。一个线程通过 ThreadLocal 保存的泛型对象实例,其他线程无法访问,当然也无需访问。

    ThreadLocal 提供两个对外方法:get() 和 set() 方法,分别用于内部对象的读写操作。

    举个例子,在工作线程中创建 ThreadLocal 对象,并存储和读取其字符串内容:

    new Thread(new Runnable() {
        @Override
        public void run() {
            ThreadLocal<String> threadLocal = new ThreadLocal<>();
            threadLocal.set("This is thread local variable");
            System.out.print(threadLocal.get());
        }
    }).start();
    

    如果将上面例子中的创建语句提取到工作线程外面的其他线程中,编译器会自动报错,提示访问受限:

    Variable "threadLocal" is accessed from within inner class,needs to be declared final

    ThreadLocal 工作原理


    搞清楚 ThreadLocal 内部工作原理,可以从前面使用简介中提到的 get() 和 set() 两个方法的源码入手。

    get() 方法源码如下:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }
    

    前面提到,ThreadLocal 与线程有关,从 get() 方法的源码中便有所体现。使用当前线程实例作为 getMap() 方法的参数,直接获取用于保存 ThreadLocal 值的 ThreadLocalMap 对象。

    getMap() 方法源码很简单,只有一行代码:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    

    即直接读取 Thread 类中定义的 ThreadLocalMap 变量:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    再回到 get() 方法源码中,如果当前线程 threadLocals 变量的值为 null,则调用 setInitialValue() 方法进行初始化:

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

    也就是间接调用 createMap() 方法实现初始化操作:

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

    并且初始化时使用的默认值是由 initialValue() 方法提供的默认值 null:

    protected T initialValue() {
        return null;
    }
    

    该方法为 protected 类型,这也为开发人员使用时修改系统默认值 null 提供了可能。我们只需要在创建 ThreadLocal 实例的时候或者在其子类中重写 initialValue() 方法即可,比如:

    ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "a new initial value";
        }
    };
    

    看完 get() 方法源码,再来看看 set() 方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    

    可以看到,set() 方法同样获取的是当前线程中的 ThreadLocalMap 对象进行赋值操作。其中有一行代码:

    map.set(this, value);
    

    与 get() 方法中提到的初始化操作一样,表示 ThreadLocalMap 使用 ThreadLocal 实例作为 key、将要保存的对象作为 value 这种键值对的形式保存。而 get() 方法读取的时候也是如此。

    Android 中的应用场景


    作为安卓开发人员,使用 ThreadLocal 的场景并不是很多,然而我们平时经常接触的 Handler 机制中涉及到的 Looper 类,其内部便使用到 ThreadLocal 操作。

    大家知道,一个线程中只能有一个 Looper 实例不停循环运转访问 MessageQueue 消息队列。这个 Looper 对象实例便是借助 ThreadLocal 保存在对应线程当中,Looper 类源码定义如下:

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    

    默认情况下,工作线程中使用 Looper 之前必须调用 prepare() 方法初始化(系统在主线程自动帮我们执行)。这个方法便是用来创建 Looper 对象并保存的:

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    

    所以,知道一些 ThreadLocal 的知识有助于我们加深理解 Handler 原理。

    ThreadLocal 答忧解惑


    1,ThreadLocal 只能在线程内访问吗

    其实不然。Java 还提供有一个继承自 ThreadLocal 的 InheritableThreadLocal 类,解决跨线程访问 ThreadLocal 变量的问题。

    在 Thread 类中也定义有一个 inheritableThreadLocals 变量:

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    

    当子线程创建的时候,自动使用父线程中 inheritableThreadLocals 变量保存的对象值初始化自己。(当然,该默认值也是可以由开发人员修改的。)

    private void init2(Thread parent) {
        // 省略不相关源码
        if (parent.inheritableThreadLocals != null) {
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(
                    parent.inheritableThreadLocals);
        }
    }
    

    有关 InheritableThreadLocal 的更多细节,感兴趣的朋友可以自行查阅相关源码。

    2,ThreadLocal 会引发内存泄漏吗

    答案是否定的。虽然从前面的源码中可以看出,ThreadLocal 保存的对象实例本质上归属于 Thread 所持有引用,当线程执行完毕,GC 自动回收其相关资源。

    看上去,默认情况下对象的生命周期与 Thread 生命周期一致,但是,ThreadLocalMap 在内部实现时对于 ThreadLocal 中对象的引用使用的是弱引用类型,从而规避内存泄漏的风险:

    static class ThreadLocalMap {
    
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;
    
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
    

    备注:对于 JVM 来说,内存泄漏的罪魁祸首在于强引用导致对象所占用的内存无法得到释放,而 Java 四种引用类型中的弱引用方式刚好能够解决这种问题。弱引用不会影响 GC 的垃圾回收工作,避免内存泄漏风险。同时也应当注意,使用弱引用获取到的对象实例,在使用前,需要添加空值判断。

    3,ThreadLocal 与多线程并发的关系?

    没有关系!网上有些文章误传 ThreadLocal 是用来解决多线程并发的问题,这是错误的理解。通过前文一系列的使用和原理分析不难看出,ThreadLocal 用于线程读写各自内部对象,通常除自己之外的线程没有必要也无法访问这个对象。

    而多线程并发产生的临界资源访问问题,完全可以通过 synchronized 关键字实现的同步机制(也称互斥锁机制)解决。有关 Java 并发编程的知识点,可以参考:

    http://wiki.jikexueyuan.com/project/java-concurrency/

    相关知识链

    https://dzone.com/articles/stack-vs-heap-understanding-java-memory-allocation

    关于我:亦枫,博客地址:http://yifeng.studio/,新浪微博:IT亦枫

    微信扫描二维码,欢迎关注我的个人公众号:安卓笔记侠

    不仅分享我的原创技术文章,还有程序员的职场遐想

    相关文章

      网友评论

        本文标题:Java ThreadLocal 类的知识点解读

        本文链接:https://www.haomeiwen.com/subject/xrjoyxtx.html