ThreadLocal源码解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
简介: 前几篇文章更多的是在使用层面去介绍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源码不禁惊呼:只怪自己没文化,一句卧槽走天下!

目录
相关文章
|
11天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
39 2
|
11天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
24天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
40 3
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
56 5
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
113 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
|
1月前
|
算法 Java 程序员
Map - TreeSet & TreeMap 源码解析
Map - TreeSet & TreeMap 源码解析
34 0
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
70 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
57 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
62 0
下一篇
无影云桌面