前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThreadLocal实现原理详解

ThreadLocal实现原理详解

作者头像
涤生
发布2018-08-14 14:36:39
3720
发布2018-08-14 14:36:39
举报
文章被收录于专栏:涤生的博客涤生的博客

简书 涤生。 转载请注明原创出处,谢谢!

介绍

ThreadLocal大家应该不陌生,经常在一些同步优化中会使用到它。很多地方叫线程本地变量,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。也就是对于同一个ThreadLocal,每个线程通过get、set、remove接口操作只会影响自身线程的数据,不会干扰其他线程中的数据。

ThreadLocal是怎么实现的呢?

ThreadLocal又有哪些误区呢?

源码分析

从ThreadLocal的set方法说起,set是用来设置想要在线程本地数据,可以看到先拿到当前线程,然后获取当前线程的ThreadLocalMap,如果map不存在先创建map,然后设置本地变量值。

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

那ThreadLocalMap又是什么?跟线程有什么关系?可以看到ThreadLocalMap其实是线程自身的一个成员属性threadLocals的类型。也就是线程本地数据都存在这个threadLocals应用的ThreadLocalMap中。

/**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

我们再接着看看ThreadLocalMap,跟想象中的Map有点不一样,它其实内部是有个Entry数组,将数据包装成静态内部类Entry对象,存储在这个table数组中,数组的下标是threadLocal的threadLocalHashCode&(INITIAL_CAPACITY-1),因为数组的大小是2的n次方,那其实这个值就是threadLocalHashCode%table.length,用&而不用%,其实是提升效率。只要数组的大小不变,这个索引下标是不变的,这也方便去set和get数据。

/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        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);
        }

我们再看看Entry的定义,Entry继承自WeakReference(这么做目的是什么,后面会讲到),构造方法有两个参数一个是threadLocal对象,一个是线程本地的变量。

/**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

看到这里大家应该就明白了,每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储以及交互访问。

误区

上文也提到了,Entry继承自WeakReference,大家都知道WeakReference(弱引用)的特性,只要从根集出发的引用中没有有效引用指向该对象,则该对象就可以被回收,这里的有效引用并不包含WeakReference,所以弱引用不影响对象被GC。 这里被WeakReference引用的对象是哪个呢?可以看Entry的构造方法,很容易看出指的是ThreadLocal自身,也就是说ThreadLocal自身的回收不受ThreadLocalMap的这个弱引用的影响,让用户减轻GC的烦恼。

但是不用做些什么吗?这么简单? 其实不然,ThreadLocalMap还做了其他的工作,试想一下,ThreadLocal对象如果外界没有有效引用,是能够被GC,但是Entry呢?Entry也能自动被GC吗,当然不行,Entry还被ThreadLocalMap的table数组强引用着呢。 所以ThreadLocalMap该做点什么? 我看看ThreadLocalMap的expungeStaleEntry这个方法,这个方法在ThreadLocalMap get、set、remove、rehash等方法都会调用到,看下面标红的两处代码,第一处是将remove的entry赋空,第二次处是找到已经被GC的ThreadLocal,然后会清理掉table数组对entry的引用。这样entry在后续的GC中就会被回收。

/**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

是不是这样就万事大吉了呢,不用担心GC问题了呢? 没那么简单,还是有点坑: 这里的坑与WeakHashMap垃圾回收原理中所说的类似,如果数据初始化好之后,一直不调用get、set等方法,这样Entry就一直不能回收,导致内存泄漏。所以一旦数据不使用最好主动remove。

有些同学问,如果线程资源回收了,还会泄漏吗? 当然不会,线程回收后,存在线程相关的ThreadLocalMap也会被回收,线程相关的本地数据自然也会回收。但是不要忘记ThreadLocal的使用场景,就是用来存储线程本地变量,大部分场景中,线程都是一直存活或者长时间存活。

场景

一般在连接池优化上会使用到ThreadLocal,在多线程获取连接池时,会有同步操作,影响性能。如果使用ThreadLocal,每个线程使用自己的独立连接,性能会有一定的提升。

总结

希望这遍文章能够提升大家对ThreadLocal的理解,避免踩坑,使用起来也更加了然于胸。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-01-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 涤生的博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
  • 源码分析
  • 误区
  • 场景
  • 总结
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档