ThreadLocal源码解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 前几篇文章更多的是在使用层面去介绍ThreadLocal,并没有深入去理解原理。其实学任何技术都是这样一个过程,我们最先接触到的可能是一个框架的API,然后你可能就会开始使用它;再然后会看看别人是怎么使用它的,有没有值得借鉴之处,再然后就是深入原理,看看它的底层是如何实现的,对它做一个深入的了解。下面我们进入正题,先分析一下ThreadLocal几个重要的方法。

这是ThreadLocal系列的最后一篇文章。


前几篇文章更多的是在使用层面去介绍ThreadLocal,并没有深入去理解原理。

其实学任何技术都是这样一个过程,我们最先接触到的可能是一个框架的API,然后你可能就会开始使用它;再然后会看看别人是怎么使用它的,有没有值得借鉴之处,再然后就是深入原理,看看它的底层是如何实现的,对它做一个深入的了解。

下面我们进入正题,先分析一下ThreadLocal几个重要的方法。

set


set方法其实很短,我们先看一下代码:

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

先拿到当前的线程,然后通过它去拿到一个Map,如果这个Map存在,就把value塞进去,否则就创建一个新的。

ThreadLocalMap是在ThreadLocal类里面实现的一个Map,它的Entry是一个弱引用的实现。

static class Entry extends WeakReference<ThreadLocal<?>>

每个线程对应一个自己线程私有的ThreadLocalMap,它被Thread对象持有:

// 类Thread里面定义了ThreadLocalMap的引用
ThreadLocal.ThreadLocalMap threadLocals = null;

从set方法的代码可以看到,最开始线程的threadLocals可能是空,这个时候就创建一个新的,赋值给当前线程对象:

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


get


看完上面的分析后,get方法就很好理解了。仍然是先通过getMap方法拿到当前线程对应的Map,然后从里面取出value。如果没有value,就调用ThreadLocal提供的初始化方法,初始化一个值。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}


初始化

先来看在ThreadLocal定义的的初始化方法,看起来就是一个很简单的protected方法:

protected T initialValue() {
    return null;
}

而为了更方便用户使用,ThreadLocal自己内部有一个ThreadLocal的实现类,它提供了一个函数式编程的方式来让客户端更方便地使用:

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    private final Supplier<? extends T> supplier;
    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

我们甚至可以依样画葫芦,自己新建一个FunctionedThreadLocal,实现更多的定制化。

remove

remove方法不得不提。首先我们思考一下,既然已经有了弱引用,按理说,如果线程没有持有某个value的时候,会在GC的时候自动清理掉对应的Entry,为什么会有remove方法存在?

因为我们在开发一个多线程的程序时,往往会使用线程池。而线程池的功能就是线程的复用。那如果线程池和ThreadLocal在一起就可能会造成一个问题:

  • job A和job B共用了同一个线程,
  • job A使用完ThreadLocal,ThreadLocal里面还有job A保存的值,而这个时候可能还没有清理掉,
  • job B复用线程进来了,取出来是 job A的值,可能就会造成问题。

所以在有必要的时候,可以在使用完ThreadLocal的时候,显式调用一下remove方法。remove方法的源码也比较简单,就是调用对应的entry的clear方法。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}
private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}


学习借鉴


这里总结出我们从ThreadLocal的设计中可以学习借鉴的一些点。

避免并发

这个其实挺有意思的。如果让我们自己来设计一个ThreadLocal,想要拿到当前线程对应的ThreadLocalMap,可能就会用一个Map来存这个关系:

Map<Thread, ThreadLocalMap> threadLocalMaps = new ConcurrentHashMap<>();

因为可能会有多个线程同时调用get/set方法,所以还需要对这个Map来做一些措施来保证线程安全,比如使用ConcurrentHashMap,甚至复杂的原子操作可能还需要上锁。这样其实对性能是不利的。

而JDK巧妙地把这个引用直接放到了Thread对象里面,使得多个线程不需要同时操作同一个对象。所以我们在设计代码的时候,也要有这种思维的转变。并不是说我想实现一个工具类,就一定要把所有的代码都写在这个工具类里面。要充分考虑怎样设计更合理,性能更高。

弱引用

使用弱引用可以让GC及时回收掉程序中不需要使用的对象。这个刚好适用于ThreadLocal的场景。因为很多时候线程执行完后,就销毁了。如果让我们显示去调用一个方法,就会变得非常麻烦。而且一旦忘记回收,还有可能撑满内存。

所以这点ThreadLocal做得很好,利用了弱引用的特性,与Java的设计哲学一致:你只管用,回收的事情我帮你做了!

Tips: 这里需要注意上面提到的与线程池一起使用可能存在的问题哦。

简单设计

ThreadLocal中自己定义了一个很简单的可以自动扩容的Map。它处理冲突的方式与HashMap不一样,HashMap是数组 + 链表/红黑树的方式来处理哈希冲突,而ThreadLocal实现得更简单,使用的是开放地址法,如果发生了冲突,就寻找下一个有空的位置。

开放地址法虽然效率不一定高,但胜在实现起来很简单,用在这里绰绰有余。我们在设计数据结构和算法的时候,甚至是在设计程序的时候,也有遵循够用、简单就行的原则,不用太过度设计。也就是我们常说的KISS原则:Keep it stupid and simple。

函数式编程

使用函数式编程可以让客户端更简单地实现定制化。比如ThreadLocal中的初始化方法,如果没有函数式编程,我们首先得新建一个ThreadLocal的继承类,然后复写它的initialValue方法,用起来特别不方便。

我们在设计自己的工具类的时候,想要实现一定程度的灵活性和定制化,就可以考虑利用函数式编程的便利。

巧用this

this其实我们平时用的还算比较多,最多的地方应该是POJO类了。但ThreadLocal进行了一个骚操作。

我们看ThreadLocalMap的源码可以发现,它的key类型就是ThreadLocal。我们在调用get/set方法的时候,就会使用this。

为什么要这么设计?你会发现Thread和ThreadLocal其实是多对多的关系。一个Thread可能会用到多个ThreadLocal,而一个ThreadLocal又同时给多个Thread用。那么问题来了,我们的入口是ThreadLocal对象,那如何能够快速地拿到当前Thread,当前ThreadLocal的value?

这就是this的关键之处了,我先拿到当前Thread,然后通过Thread里面保存的引用,拿到ThreadLocalMap,这个Map里面保存了此线程对应的所有ThreadLocal的对象,key就是这个对象本身,所以用this作为key,可以快速找到当前ThreadLocal对应的value。

假如我们要实现一个多对多的场景,比如一个学生有多个老师,一个老师有多个学生。通过学生类作为入口进去,如何能够快速获取一个学生指定老师的分数?我们写个程序来模拟一下:

// 教师类
public class Teacher {
    // 每个教师保存了自己每个学生的分数
    Map<Student, Integer> scores = new HashMap<>();
    public Map<Student, Integer> getScores() {
        return scores;
    }
}
// 学生类
public class Student {
    public int get(Teacher teacher) {
        Map<Student, Integer> scores = teacher.getScores();
        return scores.get(this);
    }
    public void set(Teacher teacher, int score) {
        teacher.getScores().put(this, score);
        Map<Student, Integer> scores = teacher.getScores();
    }
}

当然了,这种场景其实并不多见。但ThreadLocal有它的特殊性,首先当前Thread对象是可以通过全局直接获取到的,然后我们的操作入口一般是ThreadLocal,使用而不是Thread。

试想一下,其实如果JDK开放权限,通过Thread也能拿到最后的ThreadLocal,无非就是麻烦一些:大概长这样:

Thread thread = Thread.currentThread();
// 如果jdk提供下面这个方法
ThreadLocalMap threadLocalMap = thread.getThreadLocals();
threadLocalMap.set(threadLocal, value); // set
Object value = threadLocalMap.get(threadLocal); // get

但是这样一看,显然不如现在这样设计得优雅:

threadLocal.set(value); //set
Object value = threadLocal.get(); // get

所以这就是程序设计的哲学,大佬设计出来的东西,就是好用!JDK把ThreadLocal的引用放到了Thread里面,让它能够避免多个线程争用资源,再巧妙利用了this关键字,让你可以很简单地使用它。然后还考虑到了内存回收的问题,用弱引用帮你解决。

看完ThreadLocal源码不禁惊呼:只怪自己没文化,一句卧槽走天下!

目录
相关文章
|
2月前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
2月前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
2月前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
21天前
|
自然语言处理 数据处理 索引
mindspeed-llm源码解析(一)preprocess_data
mindspeed-llm是昇腾模型套件代码仓,原来叫"modelLink"。这篇文章带大家阅读一下数据处理脚本preprocess_data.py(基于1.0.0分支),数据处理是模型训练的第一步,经常会用到。
40 0
|
3月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
72 12
|
2月前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
2月前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
3月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
124 2
|
4月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
98 1
|
4月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
81 0

推荐镜像

更多