聊聊ThreadLocal实现原理

ThreadLocal是什么

ThreadLocal本地线程变量,线程自带的变量副本即每一个线程副本都有一个专属的本地变量,主要解决的就是让每一个线程绑定自己的值,自己用自己的,不跟别人争抢。通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题。在并发编程中我们知道synchronized和Lock是通过加锁的方式来保证线程安全,就好比有个管理员,现在大家签到,多个同学(线程),但是只有一只笔,只能同一个时间,只有一个线程(同学)签到,加锁(同步机制是以时间换空间,执行时间不一样,类似于排队)。聊聊ThreadLocal实现原理

ThreadLocal,Ω,每个同学手上都有一支笔,自己用自己的,不用再加锁来维持秩序(同步机制是以空间换时间,为每一个线程都提供了一份变量的副本,从而实现同时访问,互不干扰同时访问)聊聊ThreadLocal实现原理案例,我们就拿销售卖房子,来统计每个销售卖了多少房子?

class House {

    int saleCount = 0;
    public ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    // 使用synchronized 统计总共卖出多少套房子
    public synchronized void synSaleHouse() {
        ++saleCount;
    }

    //  统计每个销售员卖出多少套房子
    public void saleHouse() {
        Integer value = threadLocal.get();
        value++;
        threadLocal.set(value);
    }
}

通过上面的案例我们能出每个Thread内有自己的实例副本且该副本只由当前线程自己使用,并且在使用的时候需要统一设置初始值,但是每个线程对这个值的修改都是各自线程相互对立的。使用synchronized只能统计各个线程的总数,而ThreadLocal每个线程都有自己的一份,大家各自安好,没必要抢夺。注意的点是:在使用完ThreadLocal一定要执行remove(),防止内存溢出。

源码解析

Thread,ThreadLocal,ThreadLocalMap关系?

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}   
public class ThreadLocal<T{

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
     }
}

总结一下就是「Thread」类中有一个「ThreadLocal.ThreadLocalMap threadLocals = null」的变量,这个「ThreadLocal」相当于是「Thread」类和「ThreadLocalMap」的桥梁,在ThreadLocal中有静态内部类ThreadLocalMap,ThreadLocalMap中有Entry数组当我们为「ThreadLocal」变量赋值,实际上就是以当前「ThreadLocal」实例为key,值为value的Entry往这个threadLocalMap中存放。t.threadLocals = new ThreadLocalMap(this, firstValue) 如下这行代码,可以知道每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本。聊聊ThreadLocal实现原理

ThreadLocalMap 为什么使用弱引用?

「防止ThreadLocal对象无法被回收。」每个Thread中都存在一个ThreadLocalMap,并且ThreadLocalMap中的key是以「ThreadLocal」实例。每个key都弱引用指向「ThreadLocal」。所以当把「ThreadLocal」实例置为null以后,没有任何强引用指向「ThreadLocal」实例,所以「ThreadLocal」就可以顺利被gc回收。假如每个key都强引用指向「ThreadLocal」,那么这个「ThreadLocal」就会因为和entry存在强引用无法被回收,造成内存泄漏。除非线程结束,线程被回收了,map也跟着回收。

ThreadLocal的set(T value)方法

/*
* 向ThreadLocal中赋值,如果map为空,则创建ThreadLocalMap
*/

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value); // 调用ThreadLocalMap的set方法
        else
            createMap(t, value);  // 创建新的ThreadLocalMap实例
    }

当我们创建ThreadLocal后,第一次调用set方法赋值的时候,由于ThreadLocalMap还没有被创建,所以会执行**createMap(t, value)**方法来对ThreadLocalMap进行初始化。其中,源码如下所示:

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

从上面源码中我们可以看到,ThreadLocalMap是当前线程Thread的一个全局变量。从这里,我们就可以看出来,为什么说ThreadLocal是当前线程的本地变量了。而在ThreadLocalMap的构造方法里,蕴含着初始化创建table数组的逻辑,源码如下所示:

/**
 * 初始化ThreadLocalMap
 * 创建底层数据存储的Entr[] 数组table
 * 根据hashCode 计算出所在数组的下标i
 * 执行赋值的操作
 * 初始化代表table中元素个数i的值
 * 初始化闸门threshold
 */

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //默认大小16
            table = new Entry[INITIAL_CAPACITY];
            // 确定要插入的数组下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //创建插入的Entry对象
            table[i] = new Entry(firstKey, firstValue);
            // 设置数组table中Entry元素的个数为1
            size = 1;
            // 设置数组table的闸值
            setThreshold(INITIAL_CAPACITY);
        }

从上面源码中我们可以看到,数组默认大小是16,设定的阈值为0.75倍的数组长度,并且根据传入的参数,创建了table数组中的第一个Entry元素对象。其中,size用来记录数组中存在的Entry元素的个数。我们再来看**map.set(this, value)**方法的相关源码和注释:

 private void set(ThreadLocal<?> key, Object value) {

        
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); // 计算数组的下标
            // 我们知道Entry对象是弱引用对象,这里的遍历逻辑,先通过hash找到数组下标,
            // 然后寻找相等的ThreadLocal对象,找不到就往下一个index找
          // 有三种情况会跳出循环:
            // 找到相同key的ThreadLocal对象,然后更新value值
            // 找到数组中的一个元素Entry,但是key=null,说明虚引用是可以被gc回收的状态
            // 一直往数组下一个index查找,直到下一个index对应的元素为null
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
   }
}

ThreadLocal.get()方法

public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        //  获取此线程对象中维护的ThreadLocalMap对象
        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.remove()方法

首先获取当前线程,并根据当前线程获取一个Map,如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry.

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

Entry的key为null 原理解析

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用它,就会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(实际业务中使用线程池),这些key为null的Entry的value就会一直存在一条强引用链。虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是指向的value对象是需ThreadLocalMap调用get、set时发现key为null时才会去回收。所以弱引用其实也不能100%保证内存不泄露。所以在实际的业务开发中我们要在不使用某个ThreadLocal对象后,手动调用remove()方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

总结

ThreadLocal本地线程变量,以空间换时间,线程自带的变量副本,每个线程独一份,避免了线程安全问题。ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题。ThreadLocal使用完后一定要调用remove()方法,尤其在线程池中。

👉 如果本文对你有帮助的话,欢迎点赞|在看,非常感谢

原文始发于微信公众号(阿福聊编程):聊聊ThreadLocal实现原理

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/105792.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!