美文网首页
ThreadLocal 源码设计分析

ThreadLocal 源码设计分析

作者: Parallel_Lines | 来源:发表于2020-05-09 16:44 被阅读0次

    本文将通过分析 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 的源码设计非常巧妙,不应该仅仅会用,更应该熟悉其源码背后的设计思路。

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

    相关文章

      网友评论

          本文标题:ThreadLocal 源码设计分析

          本文链接:https://www.haomeiwen.com/subject/jkoknhtx.html