ThreadLocal | 基础知识

一、底层结构

ThreadLocal底层有一个默认容量为16的数组组成,k是ThreadLocal对象的引用,v是要放到TheadLocal的值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

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

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

数组类似为 HashMap,对哈希冲突的处理不是用链表/红黑树处理,而是使用链地址法,即尝试顺序放到哈希冲突下标的下一个下标位置

该数组也可以进行扩容

二、工作原理

一个 ThreadLocal 对象维护一个 ThreadLocalMap 内部类对象,ThreadLocalMap 对象才是存储键值的地方

更准确的说,是 ThreadLocalMap 的 Entry 内部类是存储键值的地方

见源码 set(),createMap() 可知

因为一个 Thread 对象维护了一个 ThreadLocal.ThreadLocalMap 成员变量,且 ThreadLocal 设置值时,获取的 ThreadLocalMap 正是当前线程对象的 ThreadLocalMap

// 获取 ThreadLocalMap 源码
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

所以每个线程对 ThreadLocal 的操作互不干扰,即 ThreadLocal 能实现线程隔离

三、使用

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Java");
String i = threadLocal.get()
// i = Java

四、为什么 ThreadLocal.ThreadLocalMap 底层是长度 16 的数组?

对ThreadLocal的操作见第三点,可以看到ThreadLocal每次set方法都是对同个key(因为是同个ThreadLocal对象,所以key肯定都是一样的)进行操作

如此操作,看似对ThreadLocal的操作永远只会存1个值,那用长度为1的数组它不香吗?为什么还要用16长度呢?

好了,其实这里有个需要注意的地方,ThreadLocal是可以存多个值的,代码如下:

ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("Java");
ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
threadLocal2.set("Java2");

按代码执行后,看着是new了2个ThreadLocal对象,但实际上,数据的存储都是在同一个ThreadLocal.ThreadLocalMap 上操作的

再次强调:ThreadLocal.ThreadLocalMap 才是数据存取的地方,ThreadLocal只是api调用入口,真相在ThreadLocal类源码的getMap()

因此上述代码最终结果就是一个ThreadLocalMap存了2个不同ThreadLocal对象作为key,对应value为Java,Java2

我们在看下ThreadLocal的set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    // 这里每次 set 之前,都会调用 getMap(t) 方法,t 是当前调用 set 方法的线程
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

// 重点:返回调用 set 方法的线程(例子是主线程)的 ThreadLocal 对象。  
// 所以不管 api 调用方 new 多少个 ThreadLocal 对象,它永远都是返回调用线程(例子是主线程)的 ThreadLocal.ThreadLocalMap 对象供调用线程去存取数据。
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// t.threadLocals 的声明如下
ThreadLocal.ThreadLocalMap threadLocals = null;

// 仅有一个构造方法
public ThreadLocal() {
}

五、数据存放在数组中,如何解决hash冲突问题

使用链地址法解决

具体怎么解决?看看执行get、set方法的时候:

  • set:
    ·1.数组的key等于该ThreadLocal,则覆盖该位置元素
    ·2.否则就找下一个空位置,直到找到空或者key相等为止
    ·3.根据ThreadLocal对象的hash值,定位到ThreadLocalMap数组中的位置
    ·4.如何位置无元素而直接放到该位置
    ·5.如果有元素,则覆盖该位置元素
  • get:
    ·1.根据ThreadLocal对象的hash值,定位到ThreadLocalMap数组中的位置
    ·2.如果不一致,就判断下一个位置
    ·3.否则则直接取出
// 数组元素结构
Entry(ThreadLocal<?> k, Object v) {
    super(k);
    value = v;
}

六、ThreadLocal的内存泄露隐患

三个前置知识:

  • ThreadLocal对象维护一个ThreadLocalMap内部类
  • ThreadLocalMap对象又维护一个Entry内部类,并且该类继承弱引用WeakReference<ThreadLocal<?>>,用来存放作为key的ThreadLocal对象(可见最下方的Entry构造方法源码),可见最后的源码部分
  • 不管当前内存空间是否充足,GC的JVM会回收弱引用的内存

因为ThreadLocal作为弱引用被Entry中的key变脸引用,所以如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收

这个时候Entry中的key已经被回收,但value因为是强引用,所以不会被垃圾收集器回收。这样ThreadLocal的线程如果一直持续运行,value就一直得不到回收,导致发生内存泄露

如果想要避免内存泄漏,可以使用ThreadLocal对象的remove()方法

七、为什么ThreadLocalMap的key是弱引用

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

为什么要这样设计,这样分为两种情况来讨论:

  • key使用强引用:只有创建ThreadLocal的线程还在运行,那么ThreadLocalMap的键值就都会内存泄漏,因为ThreadLocalMap的生命周期同创建它的Thread对象
  • key使用弱引用:是一种挽救措施,起码弱引用的值可以被及时GC,减轻内存泄漏。另外,即使没有手动删除,作为键的ThreadLocal也会被回收。因为ThreadLocalMap调用set、get、remove时,都会先判断之前该value对应的key是否和当前调用的key相等。如果不相等,说明之前的key已经被回收了,此时value也会被回收。因此key使用弱引用是最优的解决方案

八、父子线程如何共享ThreadLocal数据

  1. 主线程创建InheritableThreadLocal对象时,会为t.inheritableThreadLocals变量创建ThreadLocalMap,使其初始化。其中t是当前线程,即主线程
  2. 创建子线程时,在Thread的构造方法,会检查其父线程的inheritableThreadLocals是否为null。从第1步可知不为null,接着将父线程的inheritableThreadLocals变量值复制给这个子线程
  3. inheritableThreadLocals重写了getMap, createMap,使用的都是Thread.inheritableThreadLocals 变量

如下:


第 1 步:对 InheritableThreadLocal 初始化
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

第 2 步:创建子线程时,判断父线程的 inheritableThreadLocals 是否为空。非空进行复制
// Thread 构造方法中,一定会执行下面逻辑
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

第 3 步:使用对象为第 1 步创建的 inheritableThreadLocals 对象
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
}

// 示例:
// 结果:能够输出「父线程-Java」
ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("父线程-Java");
Thread t = new Thread(() -> System.out.println(threadLocal.get()));
t.start();

// 结果:null,不能够输出「子线程-Java」
ThreadLocal threadLocal2 = new InheritableThreadLocal();
Thread t2 = new Thread(() -> {
    threadLocal2.set("子线程-Java");
});
t2.start();
System.out.println(threadLocal2.get());
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 最近发现自己的Java基础知识还是有点薄弱,刚好有点空闲时间进行再补一补,然后进行整理一下,方便自己以后复习。其实...
    Steven_SHH阅读 3,054评论 0 2
  • 1. 背景 ThreadLocal源码解读,网上面早已经泛滥了,大多比较浅,甚至有的连基本原理都说的很有问题,包括...
    小陈阿飞阅读 1,353评论 2 56
  • java并发之ThreadLocal 知识导读 ThreadLocal主要作用于线程的上下文,而不是线程安全,如果...
    立志java阅读 330评论 0 0
  • ThreadLocal三个主要方法 set方法,用于设置当前线程本地变量的值,传入的参数为要设置的值。比如 thr...
    16325阅读 91评论 0 0
  • 1. 背景 ThreadLocal源码解读,网上面早已经泛滥了,大多比较浅,甚至有的连基本原理都说的很有问题,包括...
    时之令阅读 636评论 1 5