ThreadLocal 难点解析

一般性的介绍(内存泄漏,基本用法,应用场景,最佳实践等)官网和其他博客都说的很清楚,这里主要记录一下我认为threadlocal的最核心的地方和难点。

主要会包括以下方面:1. 内存泄漏问题,对象引用关系 2. threadLocalHashCode值的选取 3. 深入探究set方法。

注:本文中代码选自jdk8。

其实这两个问题,乃至其他的问题我认为都是对象引用关系,这个是最核心的。

1. 内存泄漏问题,对象引用关系

这个图引用自(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)架构师修炼宝典 这个公众号。

上图已经将引用关系讲的十分清楚了,由于每个线程对他自己的ThreadLocal持有的引用是放在线程私有的栈里面的,那一旦这个线程丢失了对自己的ThreadLocal的引用之后,如果gc了,那么这个线程里面的ThreadLocalMap里面的对应的Entry的key对ThreadLocal的weakReference将会被回收,那么这里的value值将会变成不可达状态,如果这个entry没有及时被remove掉,那么就会导致内存泄漏问题。

2. threadLocalHashCode值的选取

先说说这个是什么 看代码:

    /**
     * Get the entry associated with key.  This method
     * itself handles only the fast path: a direct hit of existing
     * key. It otherwise relays to getEntryAfterMiss.  This is
     * designed to maximize performance for direct hits, in part
     * by making this method readily inlinable.
     *
     * @param  key the thread local object
     * @return the entry associated with key, or null if no such
     */
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

实际上就是每个threadlocal对应的hashCode!看看他的生成原理:

private final int threadLocalHashCode = nextHashCode();

继续

/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

就是每个threadlocal的hash值就是i*HASH_INCREMENT+HASH_INCREMENT;

i为第i个出引用这个threadLocalHashCode的线程。

注意这里是final修饰的threadLocalHashCode值,每个线程的threadlocal只会有一次赋值的机会。

这个数是魔数,由于每一次扩容都是2的整数次方,用这个算法产生的hash值能够均匀地落到数组的每一个小格子里面,也就是说如果正常流程添加满,是不会冲突的,是可以达到空间效益最大化并且减少hash冲突的。(当然不可能会有加满的时刻,threadlocalMap在被加满前会扩容的)

(https://mp.weixin.qq.com/s/bxIkMaCQ0PriZtSWT8wrXw##)

3.深入探究set方法。

在我们看最核心的代码,set方法,前先看看简单的get,最终get会调用set的。

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    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();
}

这里的getMaps一开始我还以为是把t作为key值去map里面查,实际上不是的,是返回当前thread持有的threadlocalmap。

    /**
     * Get the entry associated with key.  This method
     * itself handles only the fast path: a direct hit of existing
     * key. It otherwise relays to getEntryAfterMiss.  This is
     * designed to maximize performance for direct hits, in part
     * by making this method readily inlinable.
     *
     * @param  key the thread local object
     * @return the entry associated with key, or null if no such
     */
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }

这里的get就是直接算出hashcode和len-1的与,然后去找到中国entry取值,getEntryAfterMiss这个分支我们先不看。我们先看上面的setInitialValue。

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

这里有一个小地方,就是初始化方法是在第一次调用get的时候触发。
然后我们接着看TheadLocal的内部类ThreadLocalMap.set。

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
    private void set(ThreadLocal<?> key, Object value) {

        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.

        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)]) {
            ThreadLocal<?> k = e.get();

            if (k == key) {
                e.value = value;
                return;
            }
            //走到以上都是常规逻辑,走到这里就有点奇怪了。
            /*其实这里就是为了解决内存泄漏问题的,当走到k==null且entry非空的地方就意味着,弱引用被gc了。因此这个staleentry需要被替换掉。*/
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

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

继续看replaceStaleEntry

    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;

        int slotToExpunge = staleSlot;
        //循环往前找需要被替换掉的staleslot,直到空为止,然后将最前面的i值赋值给slotToExpunge,表不可能满,肯定不能走完一个循环,下面的同理
        for (int i = prevIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = prevIndex(i, len))
            if (e.get() == null)
                slotToExpunge = i;

        //循环往后遍历这个数组
        for (int i = nextIndex(staleSlot, len);
             (e = tab[i]) != null;
             i = nextIndex(i, len)) {
            ThreadLocal<?> k = e.get();

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

                //找到相同的key之后,交换掉i和staleSlot,之后i就是坏节点,是需要被擦除的
                tab[i] = tab[staleSlot];
                tab[staleSlot] = e;

                /*如果只有staleSlot前面没有节点,那就把i擦除掉,为什么要这样,可能是因为staleSlot是冲突之后偏移更加短的值,会更加接近真实hash值*/

                if (slotToExpunge == staleSlot)
                    slotToExpunge = i;
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                return;
            }

            /*如果这个k为空的话把这个节点变成坏节点,为什么要这样,因为staleSlot要么会拿来交换,要么就会拿来建新值,终究会变成好人,但是这个铁定是坑爹的,如果交换了之后,在下面的cleanSomeSlots之后不久交换后的坑爹也会被删掉*/
            if (k == null && slotToExpunge == staleSlot)
                slotToExpunge = i;
        }

        // 如果找不到相同的key,那就直接赋值
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);

        // 如果这两个值不等才去删除,因为这样就找到了坑爹货,staleSlot到了这一步都是好人
        if (slotToExpunge != staleSlot)
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }

看expungeStaleEntry

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

        // 能进来的这个方法的staleSlot都铁定是坑爹的。
        tab[staleSlot].value = null;
        tab[staleSlot] = null;
        size--;

        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--;
        //一直对当前的entry里面的key进行修正位置,(有可能之前的线性探测之后,位置偏移过多,优化表),这里总会遇到空的, 因为表不可能满,会扩容。
            } 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;
    }

最后我们看cleanSomeSlot

    private boolean cleanSomeSlots(int i, int n) {
        boolean removed = false;
        Entry[] tab = table;
        int len = tab.length;
        do {
            //下面的代码是清除操作e.get获取的是key值,也就是threadlocal
            i = nextIndex(i, len);
            Entry e = tab[i];
            if (e != null && e.get() == null) {
                n = len;
                removed = true;
                i = expungeStaleEntry(i);
            }
        } while ( (n >>>= 1) != 0);
        return removed;
    }

这个方法一开始看是不太好理解为什么要循环这个次数:(n >>>= 1) != 0。实际上代码注释也说了,这个是一个tradeoff,可以理解为预期。如果遇到的空值了,理解为可能内存泄漏情况严重,而且刚gc过,那就是继续加大预期继续清除。

原文地址:https://www.cnblogs.com/kobebyrant/p/11229626.html

时间: 2024-08-01 01:56:03

ThreadLocal 难点解析的相关文章

Android重难点解析——面试中可能被问到的那些问题

这篇项目主要介绍Android中的一些重难点概念,也包括面试中可能被问到的经典问题. 因为这些知识点比较琐碎,不太适合写成一篇文章,所以采用Github管理,内容会首先在Github更新,这里不定时同步,如果你想第一时间收到通知,请关注Github中的该项目. 项目地址 Android重难点解析,欢迎star,follow,将持续分享Android开发知识 文章列表 谈谈你对Application类的理解 Android为什么要设计出Bundle而不是直接使用HashMap来进行数据传递? 谈谈

搜索引擎——用户搜索意图的理解及其难点解析,本质是利用机器学习用户的意图分类

用户搜索意图的理解及其难点解析 搜索引擎涉及的技术非常的繁复,既有工程架构方面的,又有算法策略方面的.综合来讲,一个搜索引擎的技术构建主要包含三大部分: 对 query 的理解 对内容(文档)的理解 对 query 和内容(文档)的匹配和排序 (点击放大图像) 我们今天主要探讨其中的 Query Understanding,即对 query 的理解.对 query 的理解, 换句话说就是对用户搜索意图的理解.先看垂直搜索中的一些例子: "附近的特价酒店" "上海到扬州高速怎么

JDK1.8 concurrentHashMap 同步机制难点解析

jdk1.8我认为有几个主要的难点: 同步机制 红黑树的操作 数学原理(重要是基于统计值的算法选取和变量设定) 其中这里只分析同步机制中比较重要的部分. 这篇东西和上一篇文章LongAdder的原理关联性比较大,如果懂LongAdder的则忽略. 全文主要从以下几方面来讲: 为什么1.8 concurrentHashMap要重构 1.8 concurrentHashMap的基本设计 1.8concurrentHashMap的难点方法解析 个人存在的疑惑点 这个类是写博客至今研究过的难度最大的类,

电商姬:菜鸟新手卖家常遇到的难点解析!

新手在开店的过程中,常常会遇到很多的问题,这里小编总结了几个大家常常会遇到的一些重难点,给大家好好分析分析,希望能给大家带来帮助! 一.新品如何快速达标,获取个性化流量? 这个基本就是新手的必修课了,首先,新品可以通过付费推广引流,添加精准标签词,制定目标流量1000个左右.然后可以利用页面促销或者客服咨询时折扣促成转化.之后当精准流量和精准转化都形成之后可以通过卖家中心会员关系管理体验新版精准圈人特征画像特征关键词来确定人群,达成个性化流量. 二.新品如何快速提高关键词人气权重? 首先我们需要

物理隔离下的数据交换平台难点解析与实践(一)

目录 第零章.前言:为什么?做什么?怎么做? 第一章.数据交换平台的一些基本概念 目录 第零章.前言:为什么?做什么?怎么做? 最近带队做了公司的一个项目,叫数据交换平台,旨在物理隔离的情况下对多端业务系统进行数据的加密.传输.监控. 正好这个项目的架构师也在公司做了一次架构层面的技术分享,我便把此次分享和我在项目实践中遇到的一些坑一起整理出来,形成本系列文章. 本系列文章介绍在物理隔离的网络条件下进行数据交换的难点,以及如何保证文件交换的可靠性和安全性,如何与业务系统做到低耦合. 本文的目的在

jdk1.8 LongAdder 难点解析

写这篇文章是因为jdk1.8 concurrentHashMap计算容量扩容前用到了这个类,所以之后就研究了一下这个类. 本文主要有几点内容: 为什么需要LongAdder这个类 LongAdder实现原理解析 为什么需要LongAdder这个类 看这个东西前先看看AtomicLong(1.5).这个类的主要功能是可以原子更新long类型的值. 实现也非常简单: /** * Atomically increments by one the current value. * * @return t

java难点解析(七)-抽象类

抽象类: abstract抽象:不具体,看不明白.抽象类表象体现.在不断抽取过程中,将共性内容中的方法声明抽取,但是方法不一样,没有抽取,这时抽取到的方法,并不具体,需要被指定关键字abstract所标示,声明为抽象方法.抽象方法所在类一定要标示为抽象类,也就是说该类需要被abstract关键字所修饰. 抽象类的特点:1:抽象方法只能定义在抽象类中,抽象类和抽象方法必须由abstract关键字修饰(可以描述类和方法,不可以描述变量).2:抽象方法只定义方法声明,并不定义方法实现.3:抽象类不可以

Express难点解析

app.js 应用程序入口文件1.// view engine setup 设置视图引擎app.set('views', path.join(__dirname, 'views'));//告诉express在views目 录下搜索所有模板app.set('view engine', 'hjs');//在这些模板上应用HJS模板引擎 2.//app.use()注册http请求的中间件,配置路由响应app.use(app.router);app.use(express.static(path.joi

JDK1.7 ConcurrentHashMap难点解析

上一节写了ReentrantLock, 那这一节就正好来写积蓄已久的1.7 concurrentHashMap了.因为1.7里面concurrentHashMap里面的segment是继承自ReentrantLock的. 我认为理解这个类有几个重点: 理解这个类的设计和Hashtable有什么不同,是怎么达到并发的. 理解这个类的一些位运算,怎么找到特定的segment和具体的entry的. 读懂put方法(这个是最难的) 个人疑点 我们知道hashmap最基本的就是有一个entry数组,其中每