ThreadLocal

什么是ThreadLocal

称为线程本地变量,当使用ThreadLocal维护变量时,每个Thread拥有一份自己的变量副本,多个线程之间互不干扰,实现线程间的数据隔离

ThreadLocal维护的变量在线程的生命周期内起作用

ThreadLocal数据结构

ThreadLocal数据结构

  • 每个线程对应一个Thread对象,每个Thread对象中,都拥有一个ThreadLocal.ThreadLocalMap成员变量
  • ThreadLocalMap类似于HashMap,维护的都是key-value键值对(HashMap维护的是数组+链表/红黑树;ThreadLocalMap维护的是数组)
  • ThreadLocalMap数组中存放的是静态内部类对象Entry(ThreadLocal<?> key, Object value)数组
    key实际上是弱引用WeakReference<ThreadLocal<?>>
    证明:ThreadLocalMap中的key为什么要用弱引用?

Java的四种引用类型

参考强引用、软引用、弱引用、虚引用

ThreadLocalMap中的key为什么要用弱引用?

// 证明 key 为弱引用
// 有强引用指向ThreadLocal对象
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("abc");
// 没有强引用指向ThreadLocal对象
new ThreadLocal<>().set("def");
// 手动垃圾回收
System.gc();
// 垃圾回收后,def 对应的 key=null
// 没有强引用时,弱引用指向的对象会被垃圾回收器回收
  • ThreadLocal没有强引用指向时,弱引用的key会被gc回收
  • value只有在线程生命周期结束或触发清理算法时才会被gc回收

ThreadLocal.set()方法

private void set(ThreadLocal<?> key, Object value) {
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    // 开放定址法查找可用的槽位(用于解决HASH冲突)
    for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // 如果槽位上已经有值,并且key相同,则替换value值
        if (k == key) {
            e.value = value;
            return;
        }
        // 如果槽位上有值,并且key已经被GC回收了,触发探测式清理,清理掉过时的条目
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 没有对应槽位,将key和value插入新槽位
    tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
    int sz = ++size;
    // 触发清理,并判断如果清理后的size达到了阈值,则进行rehash进行扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
  • HashMap解决Hash冲突的方式是链表/红黑树
  • ThreadLocalMap中用的是**开放定址法**

0x61c88647

  • 每创建一个ThreadLocal对象,hashCode增量都是这个数值
  • Integer有符号整数的0.618倍,即黄金比例,斐波那契数列
  • 使hash分布非常均匀

ThreadLocalMap扩容

private void rehash() {
    // 清理过时条目,也就是key被GC回收掉的条目 
    expungeStaleEntries();
    // Use lower threshold for doubling to avoid hysteresis
    // 使用较低的阈值以避免迟滞
    if (size >= threshold - threshold / 4)
        // 容量变为原来的两倍
        // Double the capacity of the table
        resize();
}

ThreadLocal.get()方法

public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 从线程对象t中获取ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 通过key(当前ThreadLocal对象)寻找value
        // 遵循开放定址法原则
        // 没有查找继续向后查找
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果获取不到值,则初始化一个值
    return setInitialValue();
}
  • 如果map不为null,以当前threadLocalkey进行查询,查询规则遵循开放定址法原则
  • 如果当前hash计算出的位置没有查到,继续向后查找
  • 如果最终还是没有查到结果,初始化一个写入并返回

InheritableThreadLocal

简单介绍

使用ThreadLocal时,子线程获取不到父线程通过set方法保存的数据,要想使子线程也可以获取到,可以使用InheritableThreadLocal

原理
  • 线程类Thread中有两个ThreadLocal.ThreadLocalMap成员变量,一个存放普通ThreadLocal相关信息,一个存放InheritableThreadLocal相关信息

    // 用来保存ThreadLocal相关信息
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // 用来保存InheritableThreadLocal相关信息
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
  • InheritableThreadLocal对象在调用set方法保存信息时,调用的是父类ThreadLocal对象的set方法

  • InheritableThreadLocal重写了getMapcreateMap

  • new Thread()时复制一份当前线程的inheritableThreadLocals到要创建的子线程中

    // 当前线程就是新建线程的父线程
    Thread parent = currentThread();
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    		    // 复制当前线程的inheritableThreadLocals个子线程
                this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
注意点
  1. 一般我们做异步化处理都是使用的线程池,InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题
  2. 要想在使用线程池等会缓存线程的组件情况下传递ThreadLocal到子线程中,可以使用阿里巴巴开源组件TransmittableThreadLocal
  3. 子线程中数据是从父线程拷贝来的,所以,在子线程中重新set的内容,对于父线程是不可见的

应用

  • 管理数据库连接

ThreadLocal使用注意事项

  • 内存泄漏或脏数据

    1. 通过线程池管理线程,一般使用完成后并不会进行销毁,若ThreadLocal也没有执行remove()方法,会导致数据一直存在,造成内存泄漏
    2. 如果此时ThreadLocal也是一个静态常量,下一次使用线程时,很可能能够获取到之前保存的数据,导致脏数据
    3. 使用ThreadLocal时,一定要在最后调用remove()

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值