ThreadLocal详解

前言

ThreadLocal本身不存数据, 在某个线程调用ThreadLocal的set方法保存对象时,会在当前线程创建一个ThreadLocalMap将ThreadLocal作为Entry的一部分存放进去。线程才是最终保存数据的地方。

一个线程可以绑定多个ThreadLocal, 在ThreadLocalMap内部, 是采用数组的方式来存放,每次存放和读取,都要根据当前threadLocal去遍历数组来找到对应的entry, 再去操作对应的value。

如果将Thread比作学生的话, ThreadLocal就是学科名, 一个学生可以有多个学科的成绩单,都放在自己身上,针对同一个学科, 不同的学生有不同的成绩单, 相互不影响。 你只需要告诉我一个学科名,我就能拿到对应的成绩单。

代码解读

ThreadLocal有两个方法: set/get, 先看下set方法

java 复制代码
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
      	//设置值, this指的是当前的threadLocal对象
        map.set(this, value);
    else
      	//如果当前线程没有threadLocalMap, 进行创建
        createMap(t, value);
}

//getMap操作只是获取当前线程的threadLocals对象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//createMap只是简单的创建一个ThreadLocalMap对象
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

再来看看get操作:

java 复制代码
public T get() {
    Thread t = Thread.currentThread();
  	//获取当前线程的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
      	//获取当前threadLocal的数据副本
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
          	//返回最终需要的值
            T result = (T)e.value;
            return result;
        }
    }
  	//get不到, 可以设置默认值并获取默认值
    return setInitialValue();
}

ThreadLocalMap解读

ThreadLocalMap是ThreadLocal的静态内部类, 最核心复杂的代码就在这个方法里面。

java 复制代码
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
  	//threadLocalHashCode是在threadLocal创建时候就定义好的, 多个threadLocal的这个值会不一样
  	//这是一个神奇的数字, 其目的,是为了保证能key均匀分布, 下面会给例子
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         //nextIndex表示当前下标有人了, 取下个id
         e = tab[i = nextIndex(i, len)]) {
      	 //key相同, 直接设置值
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }
      	//entry已经被回收了, 占用这个entry
        if (e.refersTo(null)) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
  	//设置新值
    tab[i] = new Entry(key, value);
    int sz = ++size;
  	//扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
java 复制代码
//获取下一个下标, 在不超过数组长度时,直接加1
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

下面是get操作代码:

java 复制代码
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
  	//大部分thread只会绑定一个threadLocal, 所以先判断是否第一个就是, 是的话直接返回
    if (e != null && e.refersTo(key))
        return e;
    else
      	//很不幸,第一个不是, 要搜下数组
        return getEntryAfterMiss(key, i, e);
}
java 复制代码
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    while (e != null) {
        if (e.refersTo(key))
            return e;
      	//过期清理
        if (e.refersTo(null))
            expungeStaleEntry(i);
        else
          	//真的是一个一个搜
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

神奇的0x61c88647

ThreadLocalMap使用开放地址法(open addressing) 来解决hash冲突,其做法就是取一个初始值,对数组长度进行取余,拿到对应的下标。然后把数据存放到数据下标的位置即可。存放下个key时,再加一次初始值,对数组长度进行取余,拿下标。

数据分布是否均匀,全看这个初始值取得好不好。ThreadLocal使用了一个0x61c88647来进行初始值计算。可以看下代码:

java 复制代码
//初始值是0
private static AtomicInteger nextHashCode =new AtomicInteger();
/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;
/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
  	//每次递增 0x61c88647
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
java 复制代码
public class ThreadLocal<T> {
    //每个ThreadLocal对象, 创建时立即获取一个hashCode
    private final int threadLocalHashCode = nextHashCode();

  	private Entry getEntry(ThreadLocal<?> key) {
    	//计算下标, hashCode和数组长度进行取余操作
    	int i = key.threadLocalHashCode & (table.length - 1);
    	Entry e = table[i];
    	if (e != null && e.refersTo(key))
        	return e;
    	else
        	return getEntryAfterMiss(key, i, e);
}

先测试下这个数字是否真得有用。

java 复制代码
public class MagicNumberTest {
    private static final AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    public static void main(String[] args) {
        int[] lenArray = new int[]{16, 32, 64, 128, 256, 512, 1024, 2048, 4096};
        for (int len : lenArray) {
            //构造一个16,32,64位长度的数组
            boolean[] pool = new boolean[len];
            for (int i = 0; i < len; i++) {
                //ThreadLocal在创建的时候, 获取一个hashCode
                int hashCode = nextHashCode();
                //对数组长度取余, 获取存储下标
                int index = hashCode & (len - 1);
                if (pool[index]) {
                    //下标冲突(使用这个魔数, 会均匀填满整个数组, 不会冲突)
                    System.out.println("duplicate: " + len + ", index:" + index);
                } else {
                    pool[index] = true;
                }
            }
        }
        System.out.println("finished");
    }
}

日志只打印了一个finished, 也可以尝试更改魔数, 可以证明这个数字真得有神奇的魔力。它就是能让数字均匀分布在整个数组上(偶数倍数组长度)

为什么是0x61c88647

首先复习一下斐波那契数列:

  • 斐波那契数列: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
  • 通项公式: 假设F(n)为该数列的第n项, 那么这句话可以写成如下形式: F(n) = F(n-1) + F(n-2)

当n趋向于无穷大时, 前一项与后一项的比值越来越逼近0.618... , 而这个值0.618就被称为黄金分割数。证明过程如下:

黄金分割数的准确值是(根号5-1)/2, 约等于0.618

0x61c88647 转成10进制是 1640531527, 而32位带符号整数的黄金分割值是 -1640531527, 实际上用 -1640531527测试也是可以完美分布的

-1640531527 通过不断的追加自身,也能得到1640531527, 所以猜测是为了使用习惯,才用了 1640531527。

只能说黄金比例很神奇!

ThreadLocal里面的Entry为什么是弱引用

弱引用的定义:

对象可以同时有弱引用和强引用。

只有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程一旦发现了只有弱引用的对象, 不管当前内容空间是否充裕,都会回收它占用的内存。

不过,由于垃圾回收期是一个优先级很低的线程, 因此不一定会很快发现只有弱引用的对象

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

回归本质, ThreadLocalMap是用来存放对象的,虽然是存放在thread中, 但是存放和提取的钥匙却是ThreadLocal对象。 如果ThreadLocal对象无法触达, 那么它在所有线程中关联的变量都无法进行访问(正常访问路径), 所以理论上应该被释放掉。

现在的问题是,ThreadLocal还在被Thread(可能多个)对象里面的threadLocals对象引用, 所以按照正常的流程, 它无法被回收, 除非主动调用了remove操作。

将ThreadLocal里面的Entry设置为弱引用, 当ThreadLocal没有强引用的时候, 就可以直接被回收。也就是Thread里面的ThreadLocalMap对Entry里面对象的引用是弱引用。

注意: WeakReference引用本身是强引用, 它内部的(T reference) 才是真正的弱引用字段, WeakReference就是一个装弱引用的容器而已。


key设置为弱引用有什么用

设置弱引用只是针对ThreadLocal来设置,也就是Entry对象里面的key, 当ThreadLocal不再有强引用时, 则会进行回收, 结果就是Entry里面的key为null了。 如果只有这一种措施, 那么Entry不会被回收, 任然有可能内存溢出。

ThreadLocal在进行set操作的时候, 如果检测到地址冲突, 会检查下key是否为存在, 如果不存在则进行替换,这样才算避免了内存溢出。

java 复制代码
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) {//作为key的ThreadLocal对象被回收了, 此Entry将会被替换
            replaceStaleEntry(key, value, i);
            return;
        }
    }

为什么用开放地址法

  1. ThreadLocal本身数据量就不会很大,使用开放地址法可以节约空间
  2. hash能够均匀分布, 效率本来就高

总结

  1. ThreadLocal本身不存对象, 只是提供了一个key和操作方法入口
  2. Thread里有个threadLocals字段, 类型是ThreadLocal.ThreadLocalMap, 它用来存放数据
  3. 比较意外的是, ThreadLocalMap是用数组来存放详细数据的, 存放的时候, 通过遍历下标来找到合适的index, 读取的时候,先读取最新的一个,没找到再依次去找
  4. InheritableThreadLocal也是一个ThreadLocal, 操作方法类似, 在Thread对象中,也有单独的存放字段: inheritableThreadLocals

参考文章

ThreadLocal原理 · 进击的java菜鸟

hash 结构的另一种形式 ------ 开放地址法 - 掘金

谈谈ThreadLocal为什么被设计为弱引用

黄金比例

阿里架构师浅析ThreadLocal源码------黄金分割数的使用_爱码仕i的技术博客_51CTO博客

分析ThreadLocal的弱引用与内存泄漏问题-Java8 - 寻觅beyond - 博客园(写的不错)

相关推荐
my_styles32 分钟前
docker-compose部署项目(springboot服务)以及基础环境(mysql、redis等)ruoyi-ry
spring boot·redis·后端·mysql·spring cloud·docker·容器
免檒2 小时前
go语言协程调度器 GPM 模型
开发语言·后端·golang
不知道写什么的作者2 小时前
Flask快速入门和问答项目源码
后端·python·flask
caihuayuan53 小时前
生产模式下react项目报错minified react error #130的问题
java·大数据·spring boot·后端·课程设计
一只码代码的章鱼3 小时前
Spring Boot- 2 (数万字入门教程 ):数据交互篇
spring boot·后端·交互
不再幻想,脚踏实地6 小时前
Spring AOP从0到1
java·后端·spring
编程乐学(Arfan开发工程师)6 小时前
07、基础入门-SpringBoot-自动配置特性
java·spring boot·后端
会敲键盘的猕猴桃很大胆7 小时前
Day11-苍穹外卖(数据统计篇)
java·spring boot·后端·spring·信息可视化
极客智谷7 小时前
Spring Cloud动态配置刷新:@RefreshScope与@Component的协同机制解析
后端·spring·spring cloud
Lizhihao_7 小时前
Spring MVC 接口的访问方法如何设置
java·后端·spring·mvc