1.三者的之间的关系

ThreadLocalMap是Thread类的成员变量threadLocals,一个线程拥有一个ThreadLocalMap,一个ThreadLocalMap可以有多个ThreadLocal。

ThreadLocalMap是ThreadLocal的内部类,ThreadLocal的set(),get(),remove()方法其实都是对ThreadLocalMap的操作。ThreadLocalMap中是以内部类Entry的形式关联ThreadLocal和对应的Value,其中Entry对ThreadLocal为弱引用(WeakReference<>).

如下图,大概描述了下三者的关系

2: 结构分析

首先看下Thread类,可以看到有个ThreadLocalMap类型的成员变量threadLocals,之后所有针对当前线程的ThreadLocal的存取,都是该变量来操作。

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

再来看下ThreadLocalMap的结构,它是ThreadLocal的内部类

 static class ThreadLocalMap {
//内部类Entry继承了弱引用()
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
//通过ThreLocal.set()保存的值
Object value;

//构造函数
Entry(ThreadLocal<?> k, Object v) {
//调用WeakReference的构造方法,实现Entry对ThreadLocal的弱引用
super(k);
value = v;
}
} /**
* 初始容量,即table的初始化大小
*/
private static final int INITIAL_CAPACITY = 16; /**
* Entry数组,用来保存每一个ThreadLocal
*/
private Entry[] table; /**
* 当前table中实际存放的Entry的数量
*/
private int size = 0; /**
* 扩容阈值,默认为0
*/
private int threshold; // Default to 0 /**
* Set the resize threshold to maintain at worst a 2/3 load factor.
设置扩容阈值的方法,可以看到ThreadLocalMap中的扩容的负载因子为2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}

3.完整流程分析

正常情况下我们使用ThreadLocal来存取变量都是这样的

        ThreadLocal<String> test = new ThreadLocal<>();
test.set("111");

首先看下ThreadLocal.set(T value)方法

    public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//根据线程获取ThreLocalMap,其实就是获取Thread的成员变量
ThreadLocalMap map = getMap(t);
//如果map!=null,则则将当前ThreadLocal进行设置
if (map != null)
map.set(this, value);
else
//map==null,则对该线程的ThreadLocalMap进行初始化
createMap(t, value);
}

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

当前线程第一次使用ThreadLocal, createMap()方法初始化ThreadLocalMap

    void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
} ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table,初始化大小为16
table = new Entry[INITIAL_CAPACITY];
//计算插入的数组下标,将threadLocael的hashcode与15进行按位异或操作
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//将新构建的Entry放到计算的数组下标上
table[i] = new Entry(firstKey, firstValue);
//table中实际长度赋值1
size = 1;
//设置扩容阈值,这个方法我们上面看到过,内部算法就是initial_capacity * 2/3
setThreshold(INITIAL_CAPACITY);
}

ThreadLocalMap已经存在,再次添加ThreadLocal

        private void set(ThreadLocal<?> key, Object value) {
//获取当前table
Entry[] tab = table;
int len = tab.length;
//计算出数组插入下标
int i = key.threadLocalHashCode & (len-1);
//从计算出的下标位置i开始遍历table数组,直到下一个元素Entry为null时停止
//这里解决Hash冲突的方法采用的线性探测法,计算出的位置有值的话就相邻的向下一直探索直到有位置
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//判断当前遍历的ThreadLocale是否和添加进来的key相等
if (k == key) {
//更新value
e.value = value;
return;
}
//如果存在Entry中ThreadLocal为null的情况,即该线程变量已过时,则对过时的Entry进行清除
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//走到这里说明目前的table中不存在该ThreadLocale,则创建新Entry放到计算的下标处
tab[i] = new Entry(key, value);
//table实际长度+1
int sz = ++size;
//if(!快速遍历一遍table判断是否存在Entry中ThreadLocal为null的情况&&当前table的实际长度>=扩容阈值) 则进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

看完了ThreadLocal的set()方法,再来看get()方法

    public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程持有的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//返回的Entry!=null的话直接返回其储存的value值
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap==null或者找不到该Entry,返回设置的默认值
return setInitialValue();
}

ThreadLocalMap!=null时调用getEntry()方法

        private Entry getEntry(ThreadLocal<?> key) {
//计算出在table中的数组下标
int i = key.threadLocalHashCode & (table.length - 1);
//获取指定下标中的E
Entry e = table[i];
//如果Entry!=&&ThreadLocal==当前的ThreadLocale,直接返回该Entry
if (e != null && e.get() == key)
return e;
else
//找到的元素不对或者位置上没有元素
return getEntryAfterMiss(key, i, e);
} private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//获取当前table
Entry[] tab = table;
//获取table长度
int len = tab.length;
while (e != null) {
//如果Entry!=null,就取出来再判断一下ThreadLocal是否相同
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
//清除掉key已失效的E
expungeStaleEntry(i);
else
//以当前的数组下标下后遍历Entry,因为set()时插入Entry发生Hash冲突时用的是线性探测法解决的,所以get()查找时也按此原则
i = nextIndex(i, len);
e = tab[i];
}
//如果遍历完table都找不到,返回null
return null;
}

get()获取时ThreadLocalMap还为空时调用的初始化方法setInitialValue()方法

    private T setInitialValue() {
//获取初始化value,该方法内部直接返回的为null
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取该线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map!=null,则用初始化的值来添加
if (map != null)
map.set(this, value);
else
//如果map==null,则用这个初始化值null和当前的这个ThreadLocal来创建ThreadLocalMap进行初始化
createMap(t, value);
return value;
}

使用完ThreadLocal,最好清除下remove()

     public void remove() {
//获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
map!=null就进行删除
m.remove(this);
} private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//获取到下标后线性探测法遍历table,找到后进行删除
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

remove()调用的关联方法:

    public void clear() {
//将Entry内部的弱引用的ThreadLocal置为null,方便下一次GC时进行对ThreadLocal对象进行回收
this.referent = null;
}

//释放table中的Entry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//将Entry的value的引用置为null,此时Entry不再持有任何引用,ThreadLocal和value的引用都已清除,
// expunge entry at staleSlot
tab[staleSlot].value = null;
//将该位置的Entry的引用置为null,此时此Entry也不再被table强引用,下次GC时也会回收
tab[staleSlot] = null;
//table实际长度-1
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();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

4.ThreadLocal内存泄漏问题分析

通过上述的源码我们ThreadLocal的使用及原理有了大致的了解,那么在使用ThreadLocal的同时很大可能会出现内存泄漏问题,下面我们来探讨下这究竟是怎么回事,图来源于网络

当一个Thread使用完ThreadLocal存储变量完,对应的ThreadLocal的引用被清除,这时候该ThreadLocal的强引用被清除,但是Thread的ThreadLocalMap中的Entry的key还存在着ThreadLocal的弱引用,当发生Young GC时该弱引用就会被清除,这时就会存在Entry中key=null,这导致该ThreadLocalMap永远访问不到该value,value就会内存泄漏,除非ThreadLocalMap对象也被清除。

这是由于Threrd和ThreadLocalMap的生命周期一样长,如果该在ThreadLocal清除后该Thread一直存活,那么就一直存在着value内存泄漏的问题。

既然使用了对ThreadLocal的弱引用出现了Entry中value的内存泄露,那为什么还要使用弱引用呢?如果变成强引用呢?

我们来看下,如果Entry中变成强引用ThreadLocal, 当外部的ThreadLocal强引用被清除后,由于Entry内部还有强引用,但外部又无法再通过ThreadLocal访问到,就会导致Entry的内存泄漏,泄漏对象变的更大,并且GC回收时也不会回收该Entry对象。

针对该内存泄漏现象,官方也做了相应的处理,我们在上面的源码中可以看到,不管是在调用ThreadLocal的set(),get()还是remove()方法每次在调用时遍历table的时候会因为hash冲突向下遍历一段距离,这遍历过程中如果有发现Entry中ThreadLocal为null的情况,会进行处理,将Entry完全清除掉,但是这个遍历的范围非常有限,很有可能遍历不到为null的那个Entry,即使set()方法在第一次插入ThreadLocal时还会进行一次快速的遍历table,但终究不是完全遍历,所以通过官方的优化,内存泄漏的问题还是不能够很好的解决。

内存泄漏的问题我们使用规范的话,完全是可以避免的:

1.在每次使用完ThreadLocal时,使用ThreadLocal.remove()方法,这样就会清除调Entry中的key和value的引用。

2.将ThreadLocal对象设置为private static 变成共享对象,让所有线程都使用该ThreadLocal对象,这样ThreadLocal就一直存在外部强引用,GC时就不会清除Entry的ThreadLocal,不出出现内存泄漏,但是加大了内存开销,尽量还是使用完就使用remove()进行处理。

另外一提:

因为线程池中的线程会存在复用,所以可以能存在读出脏数据的问题。即当线程池中某个线程使用ThreadLocal存储数据时,使用过后没有remove,等下次从线程池调用到该线程的时候,就会读到该线程上一次执行任务时的数据。所以务必需要remove()。

ps: 由于笔者水平有限,可能存在一些地方理解不正确,希望大家能够指出。

从源码看Thread&ThreadLocal&ThreadLocalMap的关系与原理的更多相关文章

  1. 解密随机数生成器(二)——从java源码看线性同余算法

    Random Java中的Random类生成的是伪随机数,使用的是48-bit的种子,然后调用一个linear congruential formula线性同余方程(Donald Knuth的编程艺术 ...

  2. 从源码看JDK提供的线程池(ThreadPoolExecutor)

    一丶什么是线程池 (1)博主在听到线程池三个字的时候第一个想法就是数据库连接池,回忆一下,我们在学JavaWeb的时候怎么理解数据库连接池的,数据库创建连接和关闭连接是一个比较耗费资源的事情,对于那些 ...

  3. 从jvm源码看synchronized

    从jvm源码看synchronized 索引 synchronized的使用 修饰实例方法 修饰静态方法 修饰代码块 总结 Synchronzied的底层原理 对象头和内置锁(ObjectMonito ...

  4. Alink漫谈(二) : 从源码看机器学习平台Alink设计和架构

    Alink漫谈(二) : 从源码看机器学习平台Alink设计和架构 目录 Alink漫谈(二) : 从源码看机器学习平台Alink设计和架构 0x00 摘要 0x01 Alink设计原则 0x02 A ...

  5. 从linux源码看epoll

    从linux源码看epoll 前言 在linux的高性能网络编程中,绕不开的就是epoll.和select.poll等系统调用相比,epoll在需要监视大量文件描述符并且其中只有少数活跃的时候,表现出 ...

  6. JNI-从jvm源码分析Thread.interrupt的系统级别线程打断原理

    前言 在java编程中,我们经常会调用Thread.sleep()方法使得线程停止运行一段时间,而Thread类中也提供了interrupt方法供我们去主动打断一个线程.那么线程挂起和打断的本质究竟是 ...

  7. 从源码看Azkaban作业流下发过程

    上一篇零散地罗列了看源码时记录的一些类的信息,这篇完整介绍一个作业流在Azkaban中的执行过程,希望可以帮助刚刚接手Azkaban相关工作的开发.测试. 一.Azkaban简介 Azkaban作为开 ...

  8. 从源码看Android中sqlite是怎么通过cursorwindow读DB的

    更多内容在这里查看 https://ahangchen.gitbooks.io/windy-afternoon/content/ 执行query 执行SQLiteDatabase类中query系列函数 ...

  9. 从源码看Android中sqlite是怎么读DB的(转)

    执行query 执行SQLiteDatabase类中query系列函数时,只会构造查询信息,不会执行查询. (query的源码追踪路径) 执行move(里面的fillwindow是真正打开文件句柄并分 ...

随机推荐

  1. 第12课 OpenGL 显示列表

    显示列表: 想知道如何加速你的OpenGL程序么?这一课将告诉你如何使用OpenGL的显示列表,它通过预编译OpenGL命令来加速你的程序,并可以为你省去很多重复的代码. 这次我将教你如何使用显示列表 ...

  2. ARM 链接配置.lds文件学习<转>

    本文由Jacky原创,来自http://blog.chinaunix.net/u1/58780/showart.php?id=462971 对于.lds文件,它定义了整个程序编译之后的连接过程,决定了 ...

  3. Oracle创建表、删除表、修改表、字段增删改 语句总结

    创建表: create table 表名 ( 字段名1 字段类型 默认值 是否为空 , 字段名2 字段类型 默认值 是否为空, 字段名3 字段类型 默认值 是否为空, ...... ); 创建一个us ...

  4. RedHat 7.0 Linux 下划分区,分区加密,配额,逻辑卷管理

    1:如何划分区: 1:明确分区的对象:xxx :fdisk /dev/xxx 2:增加一个分区:n:选择主分区或者扩展分区,"p" or "e" :默认地方开始 ...

  5. 【java+selenium3】特殊元素iframe的定位及详解(三)

    一.iframe 内联框架 1.自己写个网页,仅供理解iframe演示使用,如下 <!DOCTYPE html> <html> <head> <meta ch ...

  6. 使用 SSL 加密的 JDBC 连接 SAP HANA 数据库

    近期客户为满足安全要求,提了让业务应用使用 SSL 方式连接 SAP HANA 数据库的需求.本人查询 SAP官方文档 发现数据库支持 SSL 连接,有参数直接加到 JDBC 的 URL 后边就行了, ...

  7. [linux]centos7.4部署django+Uwsgi+Nginx

    前言:我已经写了几个接口用来部署在服务器上的,首先选择django+Uwsgi+Nginx因为配置简单,比较符合python的简单操作功能强大的特点 然后对于django的一些版本在之前的文章写了 参 ...

  8. <C#任务导引教程>练习十

    /*83,使用接口完成多继承问题 简化版*/using System;interface ITeacher{    string Name    {        get;        set;   ...

  9. [cf1491F]Magnets

    首先,只需要找到一个有磁性的位置,就可以通过$n-1$次判断其余磁铁是否有磁性,因此也就是要在$\lfloor\log_{2}n\rfloor+1$次中找到一个有磁性的位置 有一个$n-1$次的做法, ...

  10. Python系列教程-详细版 | 图文+代码,快速搞定Python编程(附全套速查表)

    作者:韩信子@ShowMeAI 教程地址:http://showmeai.tech/article-detail/python-tutorial 声明:版权所有,转载请联系平台与作者并注明出处 引言 ...