深入分析ThreadLocal

   2016-11-03 0
核心提示:欢迎转载,但请务必在明确位置注明文章出处! http://johnnyshieh.github.io/android/2016/11/02/explore-threadlocal/这篇文章主要分析Android中的ThreadLocal原理以及相关问题, 也分析与其在Java中内部实现的区别, 让大家理解ThreadLocal的使用场景与正确使

欢迎转载,但请务必在明确位置注明文章出处! http://johnnyshieh.github.io/android/2016/11/02/explore-threadlocal/

这篇文章主要分析Android中的ThreadLocal原理以及相关问题, 也分析与其在Java中内部实现的区别, 让大家理解ThreadLocal的使用场景与正确使用方法.

ThreadLocal的定义

Android源码中描述:

Implements a thread-local storage, that is, a variable for which each thread
has its own value. All threads share the same {@code ThreadLocal} object,
but each sees a different value when accessing it, and changes made by one
thread do not affect the other threads. The implementation supports
{@code null} values.

实现了线程局部变量的存储. 所有线程共享同一个ThreadLocal对象, 但是每个线程只能访问和修改自己存储的变量, 不会影响其他线程. 此实现支持存储null变量.

从上面的定义看出, 关键的地方即: ThreadLocal对象是多线程共享的, 但每个线程持有自己的线程局部变量. ThreadLocal不是用来解决共享对象问题的, 而是提供线程局部变量, 让线程之间不会互相干扰.

下面看在Android中Looper的应用, 每个线程只有一个Looper对象:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<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));
}
/**
 * Return the Looper object associated with the current thread.  Returns
 * null if the calling thread is not associated with a Looper.
 */
public static Looper myLooper() {
    return sThreadLocal.get();
}

在了解ThreadLocal的作用后, 也会产生一些疑问:

线程局部变量是怎么存储的?

是怎么做到线程间相互独立的?

接下来在分析Android的ThreadLocal源码的过程中, 理解其实现原理, 并解决上面的疑问.

ThreadLocal的实现原理

ThreadLocal有三个主要的public方法: set, get, remove.

ThreadLocal是通过set方法存储局部变量的, 所以先从set方法看起:

public void set(T value) {
    Thread currentThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values == null) {
        values = initializeValues(currentThread);
    }
    values.put(this, value);
}

/**
 * Gets Values instance for this thread and variable type.
 */
Values values(Thread current) {
    return current.localValues;
}

/**
 * Creates Values instance for this thread and variable type.
 */
Values initializeValues(Thread current) {
    return current.localValues = new Values();
}

set方法中根据当前线程获得 Values , 线程局部变量也是存储在 Values 中, 而不是ThreadLocal对象中. 如果一开始values为null, 就通过initializeValues方法初始化. 上面代码根据线程获得的 values 变量就是Thread对象的localValues变量, 可看下Thread源码中相关部分:

public class Thread implements Runnable {
    /**
     * Normal thread local values.
     */
    ThreadLocal.Values localValues;
}

接下来来看Values的定义, 了解其内部结构, 进一步清楚线程局部变量的存储细节:

/**
 * Per-thread map of ThreadLocal instances to values.
 */
static class Values {
    ...

    /**
     * Map entries. Contains alternating keys (ThreadLocal) and values.
     * The length is always a power of 2.
     */
    private Object[] table;
    ...
}

Values是用数组来存储ThreadLocal和对应的value的, 保存一个线程中不同ThreadLocal以及局部变量.Values类内部具体的细节, 推荐阅读 由浅入深全面剖析ThreadLocal . 其实table数组中没有保存ThreadLocal的强引用, 而是ThreadLocal的reference变量, 实际上就是保存ThreadLocal的弱引用.

/** Weak reference to this thread local instance. */
private final Reference<ThreadLocal<T>> reference
        = new WeakReference<ThreadLocal<T>>(this);

到这里就可以回答之前提到的两个问题, 线程局部变量是存储在Thread的localValues属性中, 以ThreadLocal的弱引用作为key, 线程局部变量作为value. 虽然每个线程共享同一个ThreadLocal对象, 但是线程局部变量都是存储在线程自己的成员变量中, 以此保持相互独立.

ThreadLocal的get方法的默认值

get方法就是取出Vaules中对应的线程局部变量, 需要注意的是在没有set的情况下, 调用get方法返回的默认值是null, 这其实是有initialValue方法确定的, 可以重写.

/**
 * Provides the initial value of this variable for the current thread.
 * The default implementation returns {@code null}.
 *
 * @return the initial value of the variable.
 */
protected T initialValue() {
    return null;
}

Java中的ThreadLocal有什么区别

Java的ThreadLocal源码与Android中的ThreadLocal不太一样, 不过大致的实现原理是一样的, Android中ThreadLocal稍微优化了一下, 更节约内存. 两者最大的区别就是存储局部变量的Values类在Java中是ThreadLocalMap类, 内部的存储方式有些不同, Java中用ThreadLocal.ThreadLocalMap.Entry来封装key和value.

static class ThreadLocalMap {
    private static final int INITIAL_CAPACITY = 16;
    private ThreadLocal.ThreadLocalMap.Entry[] table;
    private int size;
    private int threshold;
    ...
    
    static class Entry extends WeakReference<ThreadLocal> {
        Object value;

        Entry(ThreadLocal k, Object v) {
            super(k);
            this.value = v;
        }
    }
}

两者都是以ThreadLocal弱引用作为key值.

ThreadLocal的内存泄漏问题

网上有讨论说ThreadLocal有可能出现内存泄漏问题, 这的确是有可能的.

现在看下线程局部变量的引用链: Thread.localValues -> WeakReference 和 value. 如果没有其他对象引用ThreadLocal对象的话, ThreadLocal可能会被回收, 但是value不会被回收, value是强引用. 所以没有显式地调用remove的话, 的确有可能发生内存泄漏问题.

不过ThreadLocal的设计者也考虑到这个问题, 在get或set方法中会检测key是否被回收, 如果是的话就将value设置为null, 具体是调用Values的cleanUp方法实现的. 这种设计可以避免多数内存泄漏问题, 但是极端情况下, ThreadLocal对象被回收后, 也没有调用get或set方法的话, 还是会发生内存泄漏.

现在回过来看, 这种情况的发生都是基于没有调用remove方法, 而ThreadLocal的正确使用方式是在不需要的时候remove, 这样就不会出现内存泄漏的问题了.

线程局部变量真的只能被一个线程访问?

ThreadLocal的子类InheritableThreadLocal可以突破这个限制, 父线程的线程局部变量在创建子线程时会传递给子线程.

看下面的示例, 子线程可以获得父线程的局部变量值:

private void testInheritableThreadLocal() {
    final ThreadLocal<String> threadLocal = new InheritableThreadLocal();
    threadLocal.set("testStr");
    Thread t = new Thread() {
        @Override
        public void run() {
            super.run();
            Log.i(LOGTAG, "testInheritableThreadLocal = " + threadLocal.get());
        }
    };

    t.start();
}

// 输出结果为 testInheritableThreadLocal = testStr

具体的实现逻辑:

public class Thread implements Runnable {
    ...
    /**
     * Normal thread local values.
     */
    ThreadLocal.Values localValues;

    /**
     * Inheritable thread local values.
     */
    ThreadLocal.Values inheritableValues;
    ...
    
    // 线程创建会调用的方法
    private void create(ThreadGroup group, Runnable runnable, String threadName, long stackSize) {
        ...
        // Transfer over InheritableThreadLocals.
        if (currentThread.inheritableValues != null) {
            inheritableValues = new ThreadLocal.Values(currentThread.inheritableValues);
        }

        // add ourselves to our ThreadGroup of choice
        this.group.addThread(this);
    }
}

使用建议

  • ThreadLocal变量本身定位为要被多个线程访问, 所以通常定义为static

  • 在线程池的情况下, 在ThreadLocal业务周期结束后, 最好显示地调用remove方法

 
标签: Java 安卓开发
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • SDK热更之如何在SDK代码中自动插桩及如何生成补
    写在前面本文是SDKHotfix相关的SDK热更系列文章中的一篇,以下为项目及系列文章相关链接:SDKHotfix整体介绍:http://blog.bihe0832.com/sdk_hotfix_project.htmlSDKHotfix对应github地址:https://github.com/bihe0832/SDKHoxFix这篇文章主要介绍一下SDK热更
  • ASimpleCache
    ASimpleCache 是一个为android制定的 轻量级的 开源缓存框架。轻量到只有一个java文件(由十几个类精简而来)。1、它可以缓存什么东西?普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java对象,和 byte数据。2、它有什么特色?特色主要是
    02-05 Java开源
  • 原生App与javascript交互之JSBridge接口原理、
    前期调研调研对象:支付宝,微信,云之家调研文档:Android中JS与Java的极简交互库 SimpleJavaJsBridge设计需求阅读类型的业务功能页面需要由前端H5实现,需要做到服务端可控;页面界面更改减少重新发布新版本的频率;功能页面部分原型需求无法实现,需要原生
  • RxJava系列番外篇:一个RxJava解决复杂业务逻辑
    之前写过一系列RxJava1的文章,也承诺过会尽快有RxJava2的介绍。无奈实际项目中还未真正的使用RxJava2,不敢妄动笔墨。所以这次还是给大家分享一个使用RxJava1解决问题的案例,希望对大家在使用RxJava的时候有一点点启发。对RxJava还不了解的同学可以先去看看
  • 框架Robust原理解析(下)
    框架Robust原理解析(下)
    一、回顾框架原理本篇继续来看热修复框架Robust原理,在之前的一篇文章中已经详细讲解了:Robust框架原理,因为这个框架不是开源的,所以通过官方给出的原理介绍,咋们自己模拟了案例和框架逻辑的简单实践。最后在通过反编译美团app进行验证咋们的逻辑实现是
  • 使用Smalidea对无源码APK调试简介
    阅读:8最近正好也用了Smalidea,就ZZ的原贴做一些补充。可调试APP如果Android的系统属性ro.debuggable等于1(用getprop ro.debuggable验证),则所有APP都可调试。如果ro.debuggable等于0,某APP的AndroidManifest.xml中有android:debuggable=”true”,该APP
    01-06 JavaLinux
  • 年度盘点(四) | 2016 年十大 Java / Android 开发者必读好文
    年度盘点(四) | 2016 年十大 Java / Android
    2016 年已经过去,感谢大家支持开发者头条。 年度盘点第四篇: 2016 年十大 Java / Android 开发者必读好文 。 长按识别文章摘要下方二维码,即可进入文章评论页。0. 推荐几个自己写的 Java 后端相关的范例项目这里推荐几个自己写的范例项目,主要采用 SSM(S
  • 8个华丽而实用的Java图表类库
    8个华丽而实用的Java图表类库
    学习Java的同学注意了!!! 学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码: 589809992 我们一起学Java! 前段时间我们为大家分享过一些最常用的Java图表应用和Android图表应用,无论是在PC平台上还是移动平台上,图表和报
  • 第151期:一个RxJava解决复杂业务逻辑的案例
    第151期:一个RxJava解决复杂业务逻辑的案例
    第151期:一个RxJava解决复杂业务逻辑的案例深度讨论 基本特效:饿了么丝滑无缝过度搜索栏的实现 diycode 帖子优先,就给上个头条吧。Android开发 一个RxJava解决复杂业务逻辑的案例 本文给大家分享一个使用RxJava解决问题的案例,希望对大家在使用RxJava的时
  • Eclipse 集成ijkplayer demo
    Eclipse 集成ijkplayer demo
    接着上一篇在Mac上编译ijkplayer的.so,现在将这些文件夹拷贝到windows上。(在mac和winds上集成到eclipse上是一样的,只是我这mac上没有安装eclipse)。现在开始说集成到Eclipse的步骤:1 更改目录结构 以 ijkplayer-armv7a 文件夹为例,删除选中的这四个文件
    12-23 EclipseJava
点击排行