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

ThreadLocal简介

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ThreadLocal中内部类Values源码分析

先从构造方法看看吧:

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

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

 void put(ThreadLocal<?> key, Object value) {
cleanUp(); // Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1; for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index]; if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
} if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
} // Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;
return;
} // Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}

第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源码如下:

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

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

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

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

  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()方法,清除数据。

如下:

 ThreadLocal<String> t1 = new ThreadLocal<String>() {

             @Override
protected String initialValue() {
//
return "adc";
}
}; Toast.makeText(MainActivity.this, t1.get(), 0).show(); t1.remove();
t1 = null;

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

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

Android 异步消息处理机制前篇(一):深入理解ThreadLocal的更多相关文章

  1. Android 异步消息处理机制前篇(二):深入理解Message消息池

    版权声明:本文出自汪磊的博客,转载请务必注明出处. 上一篇中共同探讨了ThreadLocal,这篇我们一起看下常提到的Message消息池到底是怎么回事,废话少说吧,进入正题. 对于稍有经验的开发人员 ...

  2. Android 异步消息处理机制终结篇 :深入理解 Looper、Handler、Message、MessageQueue四者关系

    版权声明:本文出自汪磊的博客,转载请务必注明出处. 一.概述 我们知道更新UI操作我们需要在UI线程中操作,如果在子线程中更新UI会发生异常可能导致崩溃,但是在UI线程中进行耗时操作又会导致ANR,这 ...

  3. Android异步消息机制

    Android中的异步消息机制分为四个部分:Message.Handler.MessageQueue和Looper. 其中,Message是线程之间传递的消息,其what.arg1.arg2字段可以携 ...

  4. 深入探讨Android异步精髓Handler

    探索Android软键盘的疑难杂症 深入探讨Android异步精髓Handler 详解Android主流框架不可或缺的基石 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架 ...

  5. 【原创】源码角度分析Android的消息机制系列(二)——ThreadLocal的工作过程

    ι 版权声明:本文为博主原创文章,未经博主允许不得转载. 在上一篇文章中,我们已经提到了ThreadLocal,它并非线程,而是在线程中存储数据用的.数据存储以后,只能在指定的线程中获取到数据,对于其 ...

  6. Android异步处理系列文章四篇之三

    Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面Android异步处理二:使用AsyncTask异步更新UI界面Android异步处理三:Handler+Loope ...

  7. Android异步处理系列文章四篇之四 AsyncTask的实现原理

    Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面Android异步处理二:使用AsyncTask异步更新UI界面Android异步处理三:Handler+Loope ...

  8. Android异步处理系列文章四篇之一使用Thread+Handler实现非UI线程更新UI界面

    目录: Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面Android异步处理二:使用AsyncTask异步更新UI界面Android异步处理三:Handler+L ...

  9. Android异步处理系列文章四篇之二 使用AsyncTask异步更新UI界面

    Android异步处理一:使用Thread+Handler实现非UI线程更新UI界面Android异步处理二:使用AsyncTask异步更新UI界面Android异步处理三:Handler+Loope ...

随机推荐

  1. mysq参数sql_safe_updates限制范围

    在mysql运维中出现过不少因为update/delete条件错误导致数据被误更新或者删除的case,为避免类似问题的发生,可以用sql_safe_updates参数来对update/delete做限 ...

  2. java web 学习总结之 Servlet/JSP 编码问题

    Servlet和JSP编码问题 字节流: 1.得到OutputStream  字节流 OutputStream os = response.getOutputStream();   用默认编码输出数据 ...

  3. 【转载】Retina屏的移动设备如何实现真正1px的线?

    文章转载自 酷勤网 http://www.kuqin.com/ 原文链接:http://www.kuqin.com/shuoit/20150530/346322.html 原文摘要:前些日子总被人问起 ...

  4. 如何删除错误提交的 git 大文件

    早上小伙伴告诉我,他无法拉下代码,我没有在意.在我开始写代码的时候,发现我的 C 盘炸了.因为我的磁盘是苏菲只有 256G 放了代码就没空间了,于是我查找到了原来是我的代码占用了居然有 2000+M ...

  5. 什么是GUID?

    定义及格式 1.全局唯一标识符(GUID,Globally Unique Identifier)是一种由算法生成的二进制长度为128位的数字标识符.GUID主要用于在拥有多个节点.多台计算机的网络或系 ...

  6. 系统讲解CSS,前端开发最神奇的技术,新手的你一定不能错过

    前面小编带领大家重温了前端开发中最基本的HTML语言.如果你已经掌握了这门语言,那么恭喜你,可以去深入了解CSS技术了.CSS技术最主要的功能就是弥补HTML标记对在页面中显示外观的不足,对这些标记对 ...

  7. Lustre文件系统测试——obdfilter-survey测试

    Lustre文件系统测试--obdfilter-survey测试 介绍 该测试主要是在lustre文件系统工作环境下进行,将直接在ost上生成工作负载模拟并行文件访问,可准确检测盘阵在lustre文件 ...

  8. CURL常用命令记录--用于简单测试接口

    curl命令是一个利用URL规则在命令行下工作的文件传输工具.它支持文件的上传和下载,所以是综合传输工具,但按传统,习惯称curl为下载工具.作为一款强力工具,curl支持包括HTTP.HTTPS.f ...

  9. 关于echarts、layer.js和jqGrid的知识点

    使用echarts和layer.js直接去官方文档,能解决大部分问题. 但是有些问题,解释不够清楚,在这里记录一下. 1.echarts的使用 第一点:关于echarts的labelline在数据为零 ...

  10. 面试中常用排序算法实现(Java)

    当我们进行数据处理的时候,往往需要对数据进行查找操作,一个有序的数据集往往能够在高效的查找算法下快速得到结果.所以排序的效率就会显的十分重要,本篇我们将着重的介绍几个常见的排序算法,涉及如下内容: 排 ...