ThreadLcoal源码浅析

我们知道ThreadLocal用于维护多个线程线程独立的变量副本,这些变量只在线程内共享,可跨方法、类等,如下是一个维护多个线程Integer变量的ThreadLocal:

ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();

每个使用threadLocalNum的线程,可以通过形如threadLocalNum.set(1)的方式创建了一个独立使用的Integer变量副本,那么它是怎么实现的呢?我们今天就来简单的分析一下。

先看下ThreadLocal的set方法是如何实现的,源码如下:

public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap
if (map != null)
map.set(this, value); //当前线程的ThreadLocalMap不为空则直接设值
else
createMap(t, value); //当前线程的ThreadLocalMap为空则创建一个来设置值
}

是的,你没有看错,是获取当前线程中的ThreadLocalMap来设置的值,我们来看一下getMap(t)是如何实现的:

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

然后我们看到Thread中包含了一个ThreadLocalMap类型的属性:

ThreadLocal.ThreadLocalMap threadLocals = null;

到这里我们可以得出一个结论:各个线程持有了一个ThreadLocalMap的属性,通过ThreadLocal设置变量时,直接设置到了对应线程的的ThreadLocalMap属性中

那么不同的线程中通过ThreadLocal设置的值是如何关联定义的ThreadLocal变量和Thread中的ThreadLocalMap的呢?我们接着分析。

前面写到当前线程的ThreadLocalMap为空则创建一个ThreadLocalMap来设值,我们来看下createMap(t, value)的具体实现:

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
} ///////////////////
//ThreadLocalMap构造器定义如下
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private static final int INITIAL_CAPACITY = 16;

线程中threadLocals是一个ThreadLocalMap变量,其默认值是null,该线程在首次使用threadLocal对象调用set的时候通过createMap(Thread t, T firstValue)实例化。

先来看一下ThreadLocalMap,它是在ThreadLocal中定义的一个静态内部类,其内属性如下:

		/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16; /**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table; /**
* The number of entries in the table.
*/
private int size = 0; /**
* The next size value at which to resize.
*/
private int threshold; // Default to 0

其中属性private Entry[] table,用于存储通过threadLocal set 进来的变量,Entry定义如下:

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<?>>,ThreadLocal在构造器中被指定为弱引用super(k)(后面会单独讨论为何这里使用弱引用)。

至此,我们可以知道ThreadLocal和Thead的内存结构如下:

ThreadLocal的垃圾回收

网上看到很多文章都在讲ThreadLocal的内存泄露问题,所以也在这里简单说一下自己的理解。

从上面的结构可以看出ThreadLocal涉及到的要回收的对象包括:

  • ThreadLocal实例本身
  • 各线程中的threadLocalMap,其中包括各个Entry的 key, value

下面先简述java的引用,然后分别讨论ThreadLocal本身的回收和threadLcoalMap的回收

Java引用

  • 强引用(StrongReference):对象可达就不会被gc回收,空间不足时报error
  • 软引用(SoftReference):对象无其他强引用,当空间不足时才会被gc回收。
  • 弱引用(WeakReference):对象无其他强引用,gc过程扫描到就会被回收。

ThreadLocal的回收

ThreadLocal实例的引用主要包括两种:

  • ThreadLocal定义处的强引用
  • 各线程中ThreadLocalMap里的key=weak(threadLocal), 是弱引用

强引用还在的情况下ThreadLocal一定不会被回收;无强引用后,由于各个Thread中Entry的key是弱引用,会在下次GC后变为null。ThreadLocal实例什么时候被回收完全取决于强引用何时被干掉,那么什么时候强引用会被销毁呢?最简单的就是 threadLocal=null强引用被赋值为null;其它也可是threadLocal是一个局部变量,在方法退出后引用被销毁,等等。

这里来回答一下前面提到的为什么ThreadLocalMap中将key设计为弱引用,我们假设如果ThreadLocalMap中是强引用会出现什么情况?定义ThreadLocal时定义的强引用被置为null的时候,如果还有其它使用了该ThreadLocal的线程没有完成,还需要很久会执行完成,那么这个线程将一直持有该ThreadLocal实例的引用,直到线程完成,期间ThreadLocal实例都不能被回收,最重要的是如果不了解ThreadLocal内部实现,你可能都不知道还有其他线程引用了threadLocal实例。

线程结束时清除ThreadLocalMap的代码Thread.exit()如下:

   /**
* This method is called by the system to give a Thread
* a chance to clean up before it actually exits.
*/
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}

所以,对于threadLocal对象本身而言, 只要通过threadLocal=null就可以实现回收了。

各线程中threadLocalMap的回收

单从引用的角度来看,各线程中的threadLocalMap,其中包括各个Entry的key 和 value。线程(也就是Thread实例)本身一直持有threadLocalMap的强引用,只有在线程结束的时候才会被回收。而key是threadLocal对象的弱引用,当threadLocal被置为null时就会被回收,此时的Entry数组中就会出现很多key为null,但是value有值的元素,那么value在threadLocal对象为空后应该怎么回收呢?

ThreadLocal在实现的时候提供了一些方法:set/get/remove,可以在执行它们的时候调用ThreadLocalMap的方法回收ThreadLocalMap中已经失效(key=null)的entry实例。

这里就以set为例看看ThreadLocal是如何回收entry的,ThreadLocal set方法实现如下:

//ThreadLocal
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // 本次要分析的方法
else
createMap(t, value); //这里前面已经分析了
} //ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //获取当前threadLocal实例的hashcode,同时也是table的下标 //这里for循环找key,是因为hash冲突会使hashcode指向的下标不是真实的存储位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//找到了设置为新值
if (k == key) {
e.value = value;
return;
}
//entry不为null,key为null
//说明原来被赋值过,但是原threadLocal已经被回收
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果下标对应的entry为null, 则新建一个entry
tab[i] = new Entry(key, value);
int sz = ++size;
//清理threadlocal中其它被回收了的entry(也就是key=null的entry)
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//rehash
rehash();
}

看一下cleanSomeSlots的实现:

//ThreadLocalMap
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//获取下一个entry的下标
i = nextIndex(i, len);
Entry e = tab[i];
//entry不为null,key为null
//说明原来被赋值过,但是原threadLocal已经被回收
if (e != null && e.get() == null) {
n = len;
removed = true;
// 删除已经无效的entry
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
} private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length; // 回收无效entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--; // Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//entry不为null,key为null,应该回收
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//rehash的实现
//计算当前entry的k的hashcode,看是下标是否应该为i
//如果不为i说明,是之前hash冲突放到这儿的,现在需要reash
int h = k.threadLocalHashCode & (len - 1);
//h!=i 说明hash冲突了, entry不应该放在下标为i的位置
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
//找正确的位置h,但是还是有可能冲突所以要循环
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

从上面的分析我们可以看到把ThreadLocalMap中的key设计为weakReference,也使set方法可以通过key==null && entry != null判断entry是否失效

总结一下ThreadLocal set方法的实现:

  • 根据threadLocal计算hashcode找到entry[]数组对应位置设置值
  • 遍历数组找到其它失效的(entry不为null,key为null)的entry删除

内存泄露问题

ThreadLocal通过巧妙的设计最大程度上减少了内存泄露的可能,但是并没有完全消除。

当我们使用完ThreadLocal后没有调用set/get/remove方法,那么可能会导致失效内存不能及时被回收,导致内存泄露,尤其是在value占用内存较大的情况。

所以最佳实践是,在明确ThreadLocal不再使用时,手动调用remove方法及时清空。

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal是通过让线程内的ThreadLocalMap.Entry的key指向自身,来实现了对线程内对象的引用,从而可以在线程内方便的使用变量。同时因为操作的都是线程内的变量,也避免了实例线程安全的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 cleanSomeSlots 方法回收键为 null 的 Entry 对象的值(即失效实例)从而防止内存泄漏(其它的remove,get类似)
  • 在明确ThreadLocal不再使用时,手动调用remove方法及时清空

参考

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

ThreadLocal 从源码角度简单分析的更多相关文章

  1. JUC同步器框架AbstractQueuedSynchronizer源码图文分析

    JUC同步器框架AbstractQueuedSynchronizer源码图文分析 前提 Doug Lea大神在编写JUC(java.util.concurrent)包的时候引入了java.util.c ...

  2. MapReduce的ReduceTask任务的运行源码级分析

    MapReduce的MapTask任务的运行源码级分析 这篇文章好不容易恢复了...谢天谢地...这篇文章讲了MapTask的执行流程.咱们这一节讲解ReduceTask的执行流程.ReduceTas ...

  3. Activity源码简要分析总结

    Activity源码简要分析总结 摘自参考书籍,只列一下结论: 1. Activity的顶层View是DecorView,而我们在onCreate()方法中通过setContentView()设置的V ...

  4. MapReduce的MapTask任务的运行源码级分析

    TaskTracker任务初始化及启动task源码级分析 这篇文章中分析了任务的启动,每个task都会使用一个进程占用一个JVM来执行,org.apache.hadoop.mapred.Child方法 ...

  5. TaskTracker任务初始化及启动task源码级分析

    在监听器初始化Job.JobTracker相应TaskTracker心跳.调度器分配task源码级分析中我们分析的Tasktracker发送心跳的机制,这一节我们分析TaskTracker接受JobT ...

  6. MongoDB源码分析——mongod程序源码入口分析

    Edit 说明:第一次写笔记,之前都是看别人写的,觉得很简单,开始写了之后才发现真的很难,不知道该怎么分析,这篇文章也参考了很多前辈对MongoDB源码的分析,也有一些自己的理解,后续将会继续分析其他 ...

  7. FFmpeg的HEVC解码器源码简单分析:解析器(Parser)部分

    ===================================================== HEVC源码分析文章列表: [解码 -libavcodec HEVC 解码器] FFmpeg ...

  8. FFmpeg源码简单分析:libswscale的sws_scale()

    ===================================================== FFmpeg的库函数源码分析文章列表: [架构图] FFmpeg源码结构图 - 解码 FFm ...

  9. LinkedHashMap 源码详细分析(JDK1.8)

    1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题.除此之外,Linke ...

随机推荐

  1. python基础-第七篇-7.2面向对象(进阶篇)

    进入到今天的探索前,我先对上节内容进行一下回顾: 面向对象是一种编程方式,此编程方式的实现是基于对类和对象的使用 类是一个模板,模板中包装了多个函数可供使用 对象是基于类创建的,实例用于调用被包装在类 ...

  2. window 如何枚举设备并禁用该设备和启用该设备?如何注册设备热拔插消息通知?

    目前实现的功能: 1.设备枚举 2.设置设备禁用和启用 3.注册设备热拔插消息通知 4.获取设备 vid pid 数值 需要链接的库 SetupAPI.lib DeviceManager 类如下: D ...

  3. 【chainer框架】【pytorch框架】

    教程: https://bennix.github.io/ https://bennix.github.io/blog/2017/12/14/chain_basic/ https://bennix.g ...

  4. Cache与主存之间的全相联映射,直接映射和组相联映射的区别

    2017-02-22 注:本文并非原创,来自百度文库,只是觉得写得较好,故分享之.若是某人的知识产权,望告知!谢谢 1.高速缓冲存储器的功能.结构与工作原理 高速缓冲存储器是存在于主存与CPU之间的一 ...

  5. Android开发:带动画的分享效果

    这几天做了个带动画的分享页面.如今把它分享出来,假设你认为实用,请直接使用,避免反复造轮子 先看下效果图 认为仅仅是看效果图不明显.那么用手机扫描以下的二维码下载安装包:

  6. openssl生成证书server.key server.crt

    Key是私用秘钥,通常是RSA算法 Csr是证书请求文件,用于申请证书.在制作csr文件时,必须使用自己的私钥来签署申,还可以设定一个密钥. crt是CA认证后的证书文,签署人用自己的key给你签署凭 ...

  7. Mybatis框架学习总结-调用存储过程

    设计需求 查询数据库,查询得到男性或女性的数量,如果传入的参数是0查询女性,否则查询男性. 准备数据库表和存储过程 1.准备person表: CREATE TABLE person( id INT P ...

  8. SpringBoot安装和创建简单的Web应用

    SpringBoot安装 方式一: Eclipese->Help->Eclipse Marketplace ->Finde STS -> Install 注意:安装过程中挺慢, ...

  9. python高级之scrapy-redis

    目录: scrapy-redis组件 scrapy-redis配置示例 一.scrapy-redis组件 1.scrapy-redis简介: scrapy-redis是一个基于redis的scrapy ...

  10. my first ai application

    正式下手之前,先跑个demo体验以下. 1.my first ai application https://sonnguyen.ws/first-ai-application/ https://git ...