ThreadLocal 源码设计分析

2020-05-09  本文已影响0人  Parallel_Lines

本文将通过分析 ThreadLocal 源码,了解其背后的程序设计思路。

作用与场景

ThreadLocal 为不同的 Thread 提供了不同的变量副本,常用于变量在线程间隔离的场景。

举个例子

多个工作线程同时请求网络,获取数据后,对数据进行上报、转换、输出日志等操作。因此每个线程要创建一个 Data 对象用以保存、操作数据。

工作线程:

public class AskThread extends Thread {

    private Data data;

    @Override
    public void run() {
        super.run();
        if (askNet()) {
            DataHandle.pull(data); //上报
            Log.e(AskThread.class.getSimpleName(), DataHandle.parseString(data)); //转换为字符串
        }
    }

    private boolean askNet() {
        // 模拟从网络获取数据 这里省略 用下面代码模拟
        data = new Data();
        data.setMessage("网络数据");
        return true;
    }
}

数据 Bean:

public class Data {

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
    
    @Override
    public String toString() {
        return "Data{" +
                "message='" + message + '\'' +
                '}';
    }
}

数据处理者:

public class DataHandle {

    public static void pull(Data data) {
        // 模拟上报
        // 省略...
    }

    public static String parseString(Data data) {
        // 模拟将Data转为字符串信息
        return Thread.currentThread().getName() + " " + data.toString();
    }
}

这种写法很常见,由于对数据的处理需要依赖工具类 DataHandle,因此需要将依赖(Data)注入到 DataHandle 中,这就不可避免的产生耦合。

假如使用 ThreadLocal,则可以规避这个问题:

public class DataHandle {

    public static ThreadLocal<Data> sDataLocal = new ThreadLocal<>();

    public static void pull() {
        Data data = sDataLocal.get();
        // 模拟上报
        // 省略...
    }

    public static String parseString() {
        // 模拟将Data转为字符串信息
        Data data = sDataLocal.get();
        if (data != null) {
            return Thread.currentThread().getName() + " " + data.toString();
        }
        return "";
    }
}
public class AskThread extends Thread {

    @Override
    public void run() {
        super.run();
        if (askNet()) {
            DataHandle.pull();//1
            Log.e(AskThread.class.getSimpleName(), DataHandle.parseString());//2
        }
    }

    private boolean askNet() {
        // 模拟从网络获取数据
        Data data = new Data();
        data.setMessage("网络数据");
        DataHandle.sDataLocal.set(data);
        return true;
    }
}

注释 1、2 处可以看出,DataHandle 并不需要关注传参,只需要调用就可以了。

修改后的代码不仅逻辑上更加清晰、代码更加简洁,同时消除了依赖注入的耦合。

从上边也可以看出,ThreadLoacl 并不是无可替代,它只是提供了一种优雅的解决方案。

源码分析

ThreadLoacl 提供了线程的变量副本。

因此如果我们自己实现,一般会通过 Map 保存 Thread 对应的变量副本。代码如下:

public void set(Object value){
    map.put(Thread.currentThread(), value);
}

public void get(){
    map.get(Thread.currentThread());
}

它的层级关系如下:

层级关系1

这样实现有俩个问题,先说第一个:

首先,多线程操作同一个 Map,必然涉及到同步,虽然这个问题可以通过加锁轻松解决,但需要注意的是,我们设计 ThreadLocal 的初衷是创建一个线程变量副本的提供者,它不负责、也不参与任何线程共享逻辑,因此这样设计显然违背了初衷。

上述是以 ThreadLocal 为出发点,Thread 为 Key,保存变量副本。

如果我们转换一下思路,以 Thread 为出发点,ThreadLocal 为 Key 来保存变量副本呢?ThreadLocal 源码就是这样做的。来看下 set 源码:

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);
}

层级结构因此转变为:

层级关系2

每个线程都创建一个 map 来保存变量副本(也就是说 map 由 thread 本身来维护)。其中 Key 为 ThreadLocal 对象本身,Value 为保存值。

这样,当线程获取变量副本时,使用的 Map 是线程自己的,因此没有多线程共享同步问题。

俩者表达的意义也是不同的:
前者重心在 ThreadLocal 本身保存了多个线程的变量副本;
后者重心在 Thread 通过 ThreadLocal 保存了当前线程的变量副本。

我们的实现方式还有第二个问题:

除非在线程结束时手动从 map 中 remove 掉 key,否则会导致 Thread 内存泄漏。

靠手动维护的代码难以保证安全性,ThreadLocal 是怎么做的呢?

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

...

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

...

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

threadlocal 中,Map 的 Key 是弱引用,即 threadlocal 是弱引用,因此当把 threadlocal 实例置为 null 以后,没有任何强引用指向 threadlocal 实例,所以 threadlocal 将会被正常回收。即 key 不存在内存泄漏问题。

但是我们的 value 是强引用,它能正常回收吗?

我们来分析下场景。

threadlocal 置为 null,此时 value 由于与运行中线程存在强引用关系,故 value 不能回收。

场景 1:

运行线程结束,此时 value 唯一强引用断开,GC 正常回收;

场景 2:

线程运行时间长、或属于线程池复用线程。此时因为线程无法销毁、value 无法销毁,导致内存泄漏。

再次强调 map 是 thread 来维护的,所以 value 无法回收不会导致 threadlocal 内存泄漏。

但是联系实际,就会发现内存泄漏其实很难出现(或者出现时间很短)。

原因是因为一般情况下不会单独将 threadlocal 置为 null(置为 null 就说明调用它的线程不再需要这个变量副本);即使置为 null,在运行线程关闭后 value 也会正常回收;纵使在极端例子中,如上述不回收 value 的复用线程里,threadlocal 在 setgetremove 等方法里会遍历所有 key == null 的键值对,并进行了删除 value 的处理,所以开发者也可以及时的调用 remove 以避免内存泄漏。

总结

ThreadLocal 的源码设计非常巧妙,不应该仅仅会用,更应该熟悉其源码背后的设计思路。

本人能力有限,有问题欢迎及时指正。

上一篇下一篇

猜你喜欢

热点阅读