转载

ThreadLocal源码解析

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

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

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

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

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方法就很好理解了。仍然是先通过 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方法不得不提。首先我们思考一下,既然已经有了弱引用,按理说,如果线程没有持有某个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源码不禁惊呼:只怪自己没文化,一句卧槽走天下!

关于作者

我是Yasin,一个有颜有料又有趣的程序员。

微信公众号:编了个程

个人网站:https://yasinshaw.com

关注我的公众号,和我一起成长~

ThreadLocal源码解析
原文  https://juejin.im/post/5ef0bed9f265da02f937f899
正文到此结束
Loading...