关于java:深度解析ThreadLocal原理

46次阅读

共计 5136 个字符,预计需要花费 13 分钟才能阅读完成。

明天呢,和大家聊一下ThreadLocal

1. 是什么?

JDK1.2提供的的一个线程绑定变量的类。

他的思维就是:给每一个应用到这个资源的线程都克隆一份,实现了不同线程应用不同的资源,且该资源之间互相独立

2. 为什么用?

思考一个场景 :数据库连贯的时候,咱们会创立一个Connection 连贯,让不同的线程应用。这个时候就会呈现多个线程争抢同一个资源的状况。

这种多个线程争抢同一个资源的状况,很常见,咱们罕用的解决办法也就两种:空间换工夫,工夫换空间

没有方法,鱼与熊掌不可兼得也。就如咱们的 CAP 实践,也是就义其中一项,保障其余两项。

而针对下面的场景咱们的解决办法如下:

  • 空间换工夫:为每一个线程创立一个连贯。

    • 间接在线程工作中,创立一个连贯。(反复代码太多)
    • 应用ThreadLocal,为每一个线程绑定一个连贯。
  • 工夫换空间:对以后资源加锁,每一次仅仅存在一个线程能够应用这个连贯。

通过 ThreadLocal 为每一个线程绑定一个指定类型的变量,相当于线程私有化

3. 怎么用?

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.get();
threadLocal.set(1);
threadLocal.remove();

没错,这四行代码曾经把 ThreadLocal 的应用办法体现得明明白白。

  • getThreadLocal 拿出一个以后线程所领有得对象
  • set给以后线程绑定一个对象
  • remove将以后线程绑定的以后对象移除

记住在应用的当前,肯定要 remove, 肯定要 remove, 肯定要 remove

为什么要 remove。置信不少小伙伴听到过ThreadLocal 会导致内存透露问题。

没错,所以为了解决这种状况,所以你懂吧,用完就移除,别节约空间(渣男快慰)

看到这,脑袋上有好多问号呈现了(小朋友你是否有很多问号?

为啥会引发内存透露?

为啥不 remove 就内存透露了

它是怎么讲对象和线程绑定的

为啥 get 的时候拿到的就是以后线程的而不是其余线程的

它怎么实现的???

来吧,开淦,源码来

4. 源码解读

先来说一个思路:如果咱们本人写一个 ThreadLocal 会咋写?

线程绑定一个对象。这难道不是咱们熟知的 map 映射?有了 Map 咱们就能够以线程为 Key, 对象为value 增加到一个汇合中,而后各种 get,set,remove 操作,想怎么玩就怎么玩,搞定。????

这个时候,有兄弟说了。你这思路不对啊,你这一个线程仅仅只能寄存一个类型的变量,那我想存多个呢?

摸摸本人充盈的发量,你说出了一句至理名言:万般问题,皆系于源头和后果之中。

从后果思考,让开发者本人搞线程公有(预计被会开发者骂死)

来吧,从源头思考。当初咱们的需要是:线程能够绑定多个值,而不仅仅是一个。嗯,没错,兄弟们把你们的想法说进去。

让线程本人保护一个 Map,将这个 ThreadLocal 作为 Key, 对象作为Value 不就搞定了

兄弟,牛掰旮旯四


此时,又有兄弟说了。依照你这样的做法,将 ThreadLocal 扔到线程自身的的 Map 里,那岂不是这个 ThreadLocal 始终被线程对象援用,所以在线程销毁之前都是可达的,都无奈 GC 呀,有 BUG???

好,问题。这样想,既然因为线程和 ThreadLocal 对象存在援用,导致无奈 GC,那我将你和线程之间的援用搞成弱援用或者软援用不就成了。一GC 你就没了。

啥,你不晓得啥是弱援用和软援用???

后面讲过的货色,算啦再给你们温习一波。

JDK中存在四种类型援用,默认是强援用,也就是咱们常常干的事件。疯狂new,new,new。这个时候创立的对象都是强援用。

  • 强援用。间接new
  • 软援用。通过 SoftReference 创立,在内存空间有余的时候间接销毁,即它可能最初的销毁地点是在老年区
  • 弱援用。通过 WeakReference 创立,在 GC 的时候间接销毁。即其销毁地点必然为伊甸区
  • 虚援用。通过 PhantomReference 创立,它和不存也一样,十分虚,只能通过援用队列在进行一些操作,次要用于堆外内存回收

好了,回到正题,下面的援用里最适宜咱们以后的场景的就是弱援用了,为什么这个样子说:

在以往咱们应用完对象当前等着 GC 清理,然而对于 ThreadLocal 来说,即便咱们应用完结,也会因为线程自身存在该对象的援用,处于对象可达状态,垃圾回收器无奈回收。这个时候当 ThreadLocal 太多的时候就会呈现内存透露的问题。

而咱们将 ThreadLocal 对象的援用作为弱援用,那么就很好的解决了这个问题。当咱们本人应用完 ThreadLocal 当前,GC 的时候就会将咱们创立的强援用间接干掉,而这个时候咱们齐全能够将线程 Map 中的援用干掉,于是应用了弱援用,这个时候大家应该懂了为啥不应用软援用了吧

还有一个问题:为什么会引发内存透露呢?

理解 Map 构造的兄弟们应该分明,外部理论就一个节点数组,对于 ThreadLocalMap 而言,外部是一个 Entity,它将Key 作为弱援用,Value还是强援用。如果咱们在应用完 ThreadLocal 当前,没有对 Entity 进行移除,会引发内存透露问题。

ThreadLocalMap提供了一个办法 expungeStaleEntry 办法用来排除有效的 EntityKey 为空的实体)

说到这里,有一个问题我思考了蛮久的,value 为啥不搞成弱援用,用完间接扔了多好

最初思考进去得答案(依照源码推了一下):

不设置为弱援用,是因为不分明这个 Value 除了 map 的援用还是否还存在其余援用,如果不存在其余援用,当 GC 的时候就会间接将这个 Value 干掉了,而此时咱们的 ThreadLocal 还处于应用期间,就会造成 Value 为 null 的谬误,所以将其设置为强援用。

而为了解决这个强援用的问题,它提供了一种机制就是下面咱们说的将 KeyNullEntity 间接革除

到这里,这个类的设计曾经很分明了。接下来咱们看一下源码吧!


须要留神的一个点是:ThreadLocalMap解决哈希抵触的形式是线性探测法。

人话就是:如果以后数组位有值,则判断下一个数组位是否有值,如果有值持续向下寻找,直到一个为空的数组位

Set 办法

class ThreadLocal    
    public void set(T value) {
        // 拿到以后线程
        Thread t = Thread.currentThread();
    // 获取以后线程的 ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 如果以后线程的 Map 曾经创立,间接 set
            map.set(this, value);
        else
            // 没有创立,则创立 Map
            createMap(t, value);
    }

    private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            // 拿到以后数组位,以后数组位是否位 null,如果为 null,间接赋值,如果不为 null,则线性查找一个 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;
        // 革除一些生效的 Entity
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();}


    ThreadLocalMap getMap(Thread t) {
    // 获取以后线程的 ThreadLocalMap
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
            // 以后对象作为 Key,和咱们的构想一样
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

Get 办法

    public T get() {
        // 获取以后线程
        Thread t = Thread.currentThread();
        // 拿到以后线程的 Map
        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();}

    private Entry getEntry(ThreadLocal<?> key) {
        // 计算数组位
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
        // 如果以后数组有值,且数组位的 key 雷同,则返回 value
            if (e != null && e.get() == key)
                return e;
            else
                // 线性探测寻找对应的 Key
                return getEntryAfterMiss(key, i, e);
        }

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;
            int len = tab.length;

            while (e != null) {ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    // 排除以后为空的 Entity
                    expungeStaleEntry(i);
                else
                    // 获取下一个数组位
                    i = nextIndex(i, len);
                e = tab[i];
            }
        // 如果没有找到间接返回空
            return null;
        }

remove

    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();
                    // 清空位 NUll 的实体
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

咱们能够看到一个景象:在 set,get,remove 的时候都调用了 expungeStaleEntry 来将所有生效的 Entity 移除

看一下这个办法做了什么

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;
            int len = tab.length;

            // 删除实体的 Value
            tab[staleSlot].value = null;
    // 置空这个数组位
            tab[staleSlot] = null;
    // 数量减一
            size--;

            // 从新计算一次哈希,如果以后数组位不为 null,线性查找直到一个 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;
        }

更多原创内容请关注博主同名公众号

正文完
 0