ThreadLocal 可以把一个对象保存在指定的线程中,对象保存后,只能在指定线程中获取保存的数据,对于其他线程来说则无法获取到数据。

日常开发中 ThreadLocal 使用的地方比较少,但是系统在 Handler 机制中使用了它来保证每一个 Handler 所在的线程中都有一个独立的 Looper 对象,为了更好的理解 Handler 机制

ThreadLocal 是什么

ThreadLocal 位于 java.lang 包下。

ThreadLocal 是一个关于创建线程局部变量的类。

什么是线程的局部变量呢?

其实就是这个变量的作用域是线程,其他线程访问不了。通常我们创建的变量是可以被任何一个线程访问的,而使用 ThreadLocal 创建的变量只能被当前线程访问,其他线程无法访问。

使用示例

先来看看一个使用 ThreadLocal 的示例,对 ThreadLocal 有一个基本、直观的认识。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "ThreadLocalTest";
private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>(); @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); stringThreadLocal.set("MainThread");
Log.d(TAG, "MainThread's stringThreadLocal=" + stringThreadLocal.get());
new Thread("Thread#1") {
@Override
public void run() {
Log.d(TAG, "Thread#1's stringThreadLocal=" + stringThreadLocal.get());
}
}.start();
}
}

首先创建了一个 泛型为 String 的 ThreadLocal 对象,并初始化。这样就有了一个可以保存 String 类型的 ThreadLocal 对象。接着在主线程和子线程中分别操作该对象,使用 set 方法赋值,get 方法取值,注意看每个线程中的打印结果。

D/ThreadLocalTest: MainThread's stringThreadLocal = MainThread
D/ThreadLocalTest: Thread#1's stringThreadLocal = null

可以看到,MainThread 对 stringThreadLocal 的修改并没有影响到 Thread#1 中的值。说明了使用 ThreadLocal 保存的对象的作用域是当前线程。

Looper 中的使用

再来看看 Android 源码中 Looper.java 是怎样使用 ThreadLocal 的。

// sThreadLocal.get() will return null unless you've called prepare().
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));
}

这里使用 ThreadLocal 保存 Looper,确保每个线程中只有一个 Looper 对象。

修改默认值

从上面的示例中,我们看到 ThreadLocal 保存的对象默认值是 null。如果我们需要给定一个默认的值,就需要重写 initialValue 方法,该方法默认返回 null,我们可以根据具体要求返回需要的值,如下所示。

private ThreadLocal<Boolean> booleanThreadLocal = new ThreadLocal<Boolean>(){
@Override
protected Boolean initialValue() {
return false;
}
};

ThreadLocal 还有一个对外提供的方法 remove,看名字就知道这是删除已经保存的数据的。

原理

set 方法

ThreadLocal 的 public 方法,只有三个:set、get、remove。我们先从 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 方法进行了如下几部操作:

1.获取当前线程
2.使用当前线程获取一个 ThreadLocalMap 对象
3.如果获取到的 map 对象不为空,则设置值,否则创建 map 设置值

下面是 getMap 源码:

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

上面代码获取到的是 Thread 对象的 threadLocals 变量,类型为 ThreadLocal.ThreadLocalMap。

而如果 map 对象为空,则新建 ThreadLocalMap 对象。

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

结论:原来每个线程都有一个保存值的 ThreadLocalMap 对象,ThreadLocal 的值就存放在了当前线程的 ThreadLocalMap 成员变量中,所以只能在本线程访问,其他线程不能访问。

我们在看看具体的保存方法:ThreadLocalMap#set

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

上面的代码实现了数据的存储,其中 table 是一个 Entry[] 数组对象,而 Entry 是用来存储 ThreadLocal key, Object value 的,逻辑是根据 key 找出 Entry 对象,如果找出的这个 Entry 的 k 等于 key,直接设置 Entry 的 value,如果 k 为空,则通过 replaceStaleEntry 保存数据,最后构建出 Entry 保存进 table 数组中。

Entry 对象是怎样保存 key 和 value 的呢?

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

原来 Entry 继承了 WeakReference<ThreadLocal>,那么通过 Entry 对象的 get 方法就可以获取到一个弱引用的 ThreadLocal 对象。扩展了一个 Object 类型的 value 对象,并且在构造方法中进行了初始化赋值。Entry 保存了 ThreadLocal(key) 和 对应的值(value),其中 ThreadLoacl 是通过弱引用的形式,避免了线程池线程复用带来的内存泄露。

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

get 方法首先取出当前线程的 ThreadLocalMap 对象,如果这个对象为空,则返回默认值;如果不为空,使用当前 ThreadLoacl 对象(this)获取 ThreadLocalMap 的 Entry 对象,返回 Entry 保存的 value 值。

从 ThreadLoacl 的 set 和 get 方法来看,它们操作的对象都是当前线程对象中的 ThreadLocalMap 对象的 Entry[] 数组,因此在不同的线程中访问同一个 ThreadLoacl 的 set 和 get 方法,操作的对应线程中的数据,所以不会影响到其他线程。

通过Entry数组保存局部变量。通过key(ThreadLocal类型)的hashcode来计算数组存储的索引位置i。如果i位置已经存储了对象,那么就往后挪一个位置依次类推,直到找到空的位置,再将对象存放。另外,在最后还需要判断一下当前的存储的对象个数是否已经超出了阈值(threshold的值)大小,如果超出了,需要重新扩充并将所有的对象重新计算位置。

线程保存ThreadLocalMap对象,对象主要通过Entry[]数组存放键{threadlocal}值,通过threadlocal的threadLocalHashCode定位存放数组位置,Entry extendsWeakReference<ThreadLocal> 的value保存变量副本,通过Entry.get获取threadlocal。

如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。

从ThreadLocal读一个值可能遇到的情况:根据入参threadLocal的threadLocalHashCode对表容量取模得到 index

  • 如果index对应的slot就是要读的threadLocal,则直接返回结果
  • 调用getEntryAfterMiss线性探测,过程中每碰到无效slot,调用expungeStaleEntry进行段清理;如果找到了key,则返回结果entry
  • 没有找到key,返回null

ThreadLocal的set方法可能会有的情况。

  • 探测过程中slot都不无效,并且顺利找到key所在的slot,直接替换即可
  • 探测过程中发现有无效slot,调用replaceStaleEntry,效果是最终一定会把key和value放在这个slot,并且会尽可能清理无效slot
    • 在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值
    • 在replaceStaleEntry过程中,没有找到key,直接在无效slot原地放entry
  • 探测没有发现key,则在连续段末尾的后一个空位置放上entry,这也是线性探测法的一部分。放完后,做一次启发式清理,如果没清理出去key,并且当前table大小已经超过阈值了,则做一次rehash,rehash函数会调用一次全量清理slot方法也即expungeStaleEntries,如果完了之后table大小超过了threshold – threshold / 4,则进行扩容2倍

问题如下:

1、每个线程的变量副本是存储在哪里的?

可以从ThreadLocal的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();
}
其中getmap函数是用t(这里t就是当前执行的线程)作为参数,得到线程ThreadLocalMap对象的本地对象引用threadLocals
而通过map.getEntry(this)这里的this就是ThreadLocal获取到存的值
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

当前线程中,有一个变量引用名字是threadLocals,这个引用是在ThreadLocal类中createmap函数内初始化的。每个线程都有一个这样的threadLocals引用的ThreadLocalMap,以ThreadLocal和ThreadLocal对象声明的变量类型作为参数

public class Thread implements Runnable {
... /* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

从而得知,get函数就是通过getMap(t),的t.threadLocals

从当前线程的ThreadLocalMap中取出当前线程对应的变量的副本【注意,变量是保存在线程中的,而不是保存在ThreadLocal变量中】。当前线程中,有一个变量引用名字是threadLocals,这个引用是在ThreadLocal类中createmap函数内初始化的。每个线程都有一个这样的threadLocals引用的ThreadLocalMap,以ThreadLocal和ThreadLocal对象声明的变量类型作为参数。这样,我们所使用的ThreadLocal变量的实际数据,通过get函数取值的时候,就是通过取出Thread中threadLocals引用的map,然后从这个map中根据当前threadLocal作为参数,取出数据。

 

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和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)

ThreadLocal源码解读

https://www.cnblogs.com/micrari/p/6790229.html

https://www.zybuluo.com/kiraSally/note/854555

Java ThreadLocal的演化、实现和场景

https://duanqz.github.io/2018-03-15-Java-ThreadLocal

正确理解Thread Local的原理与适用场景

http://www.jasongj.com/java/threadlocal/

ThreadLocal类型变量为何声明为静态?

https://blog.csdn.net/chicm/article/details/40894299

内存泄露

https://juejin.im/post/5ba9a6665188255c791b0520

Android的消息机制之ThreadLocal的工作原理的更多相关文章

  1. Android消息机制之ThreadLocal的工作原理

    来源: http://blog.csdn.net/singwhatiwanna/article/details/48350919 很多人认为Handler的作用是更新UI,这说的的确没错,但是更新UI ...

  2. Android的消息机制

    一.简介 ①.我们不能在子线程中去访问UI空控件,这是时候只能通过Handler将更新UI的操作放到主线程中去执行 ②.Handler的组成:messageQueue和Looper的支持 ③.Mess ...

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

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

  4. 【原创】源码角度分析Android的消息机制系列(五)——Looper的工作原理

    ι 版权声明:本文为博主原创文章,未经博主允许不得转载. Looper在Android的消息机制中就是用来进行消息循环的.它会不停地循环,去MessageQueue中查看是否有新消息,如果有消息就立刻 ...

  5. 《Android开发艺术探索》读书笔记 (10) 第10章 Android的消息机制

    第10章 Android的消息机制 10.1 Android消息机制概述 (1)Android的消息机制主要是指Handler的运行机制,其底层需要MessageQueue和Looper的支撑.Mes ...

  6. 《android开发艺术探索》读书笔记(十)--Android的消息机制

    接上篇<android开发艺术探索>读书笔记(九)--四大组件 No1: 消息队列MessageQueue的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表,因为单链表 ...

  7. Android 基础 十一 Android的消息机制

    Handler是Android消息机制的上层接口,这使得在开发应用过程中我们只需要和Handler交互即可.Handler的使用过程很简单,通过它可以轻松地将一个任务切换到Handler所在的线程中去 ...

  8. Android的消息机制简单总结

    参考文章: http://gityuan.com/2015/12/26/handler-message-framework/#next 参考资料: Android Framework的源码: Mess ...

  9. 聊一聊Android的消息机制

    聊一聊Android的消息机制 侯 亮 1概述 在Android平台上,主要用到两种通信机制,即Binder机制和消息机制,前者用于跨进程通信,后者用于进程内部通信. 从技术实现上来说,消息机制还是比 ...

随机推荐

  1. new和delete用法小结

    在C语言中是利用库函数 malloc 和 free 函数来分配和撤销内存的.C++提供了较简便而功能较强的运算符 new 和 delete 来取代 malloc 和 free 函数. new 和 de ...

  2. 怎么去掉zencart模板网址后面的zenid=数字这个东西

    搜索引擎优化后第一次进入商店网址URL后面会出现zenid=XXXX 如:http://afish.cnblogs.com/zencart-zenid.html?zenid=tbisz675099db ...

  3. 2019全国卷(III)理科23题的另类解法

    已知 $x,y,z\in\textbf{R}$且$x+y+z=1$ (1)求$(x-1)^2+(y+1)^2+(z+1)^2$的最小值: (2)若$(x-2)^2+(y-1)^2+(z-a)^2\ge ...

  4. IC SPEC相关数据

    ---恢复内容开始--- 静态电流:静态电流是指没有信号输入时的电流,也就是器件本身在不受外部因素影响下的本身消耗电流. 纹波电压的害处: 1.容易在用设备中产生不期望的谐波,而谐波会产生较多的危害: ...

  5. silverlight发布设置

    HTTP头 - MIME类型.xap xapapplication/x-silverlight .xaml application/xaml+xml

  6. PowerDesigner 生成SQL Server 2005 注释脚本

    --生成数据表的注释EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=[%R%?[N]]%.q:COMMENT% , @l ...

  7. Java 建造者模式 简单的理解

    建造者模式 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式,使用多个简单的对象一步一步构建成一个复杂的对象. 意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表 ...

  8. Mac 安装RN android开发环境

    前言 前面介绍了MAC 安装,再来讲讲mac 安装 安卓的开发环境 首先貌似很多Mac自带安卓JDK ,你可以在终端上输入java -version 看是否已经有java开发环境. 如果没有java开 ...

  9. 将TextEdit设置为密码框

    属性--Properties--UseSystemPasswordChar设置为true

  10. 43 java中的异常处理机制的简单原理和应用