栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

源码阅读-java-ThreadLocal

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

源码阅读-java-ThreadLocal

原理

每个线程会绑定一个TreadLocalMap的对象threadLocals 通过TreadLocal操作当前线程里的threadLocals从而避免资源竞争 如果Thread的ThreadLocals为null 由ThreadLocal为其创建一个 即我们要的数据其实放在Thread的threadLocals里面 线程销毁时会把Thread的threadLocals置为null 让gc回收 当我们使用线程池时线程不会被回收如tomcat这时候就的小心内存泄漏了

public class ThreadLocal{
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    //浓缩的逻辑
    public void get(){
        //Thread.currentThread().threadLocals
        ThreadLocalMap map = getMap(Thread.currentThread());
        if(map == null){
            // 通过threadLocal获取entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = (T)e.value;
                return result;
            }
        }
        // entry为null 或map为null
        if(map != null){
            //往threadLocals里注册 key为threadLoal对象
            //value是我们通过threadLocal get和set 获取和设置的对象
            map.set(this, initialValue());
        }else{
            //initValue() 返回null
            createMap(t, initialValue());
        }
        
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}
问题

ThreadLocalMap内部持有一个Entry数组 Entry 的key是被WeekReference包装的threadLocal对象 当我们把threadLocal置为null后 gc时会把不可达的threadLocal对应的weekReference的value置为null 即把threadLocals中的key中的value置为null 这样只要该threadLocals对应的thread不销毁 那么 该key对应的value也不会被销毁 即一个threadLocal不可达时 所有线程的threadLocals中与只对应的value无法访问 无法回收 照成内存泄漏

优化 总纲

然而java会看着上面的问题发生而无动于衷吗 当然不会 java针对该问题进行了以下处理

getEntry 获取元素时 如果threadLocal对应下标是个空槽或陈旧数据时 将进行陈旧数据到下一个空槽之间的元素进行清洗和重新散列
陈旧数据:entry为null 或 entry.key不等于threadLocal对象 空槽:entry为null 重新散列: 检查元素下标让其出现在该出现的位置 如key计算出下标为3 那么它该出现在下标为3的槽位上 如果已经有元素 往后循环安排 直到找到空槽为止

remove 根据threadLocal计算出下标 重下标处开始查询到下一个空槽 当查看元素key等于localThread对象时 让其成为陈旧数据 并从它开始清洗和重新散列到下一个空槽

set 根据ThreadLocal获取hash下标 从下标到下一个空槽找到key相等的元素 赋值并返回(这里没优化) 如果该数据是陈旧数据 从该下标到上一个空槽中找陈旧数据 如找到 标记为待清除下标 在从该元素往后找 空槽为止 如果找到key相等的元素 跟hash下标元素交换 既出现在它该出现的位置 如果待删除下标等于hash下标 修复待删除下标为当前下标 接着从待删除下标开始到下一个空槽进行清洗和重新散列 接着从下一个空槽开始平衡时间和效果进行清洗和散列 当没找到元素且key为null时待删除下标为hash下标时修复为当前下标 为hash下标的value置为null好被gc 根据key和value构建entry 放到hash下标对应的空间 如果待清理元素不是hash下标时 开始从待删除下标开始清理 到下一个空槽 接着从下一个空槽开始平衡时间和效果进行清洗和散列 当没找到entry也没有陈旧entry 创建个entry给hash下标槽位 同时平衡清洗陈旧元素和重新散列 当没有清洗出元素 或 数量大于等于数组的3分之二 在来一次完整的清洗和散列 当数组使用了一办 进行数组扩容 重新散列旧数组中的元素到新数组 陈旧数据直接淘汰

getEntry

当要从threadLocal中获取对象时 会先从当前线程持有的threadLocals中以threadLocal对象为key获取entry 如果获取不到Entry或者获取的entry的key不等于threadLocal对象 那么将对垃圾数据(脏数据 通过threadLocal访问不到的数据)进行清洗

public class ThreadLocal{
    // 连续生成的哈希码之间的差异 将隐式顺序线程本地 ID 转换为近乎最优分布的乘法哈希值,用于 2 次方大小的表。
    // 该自增值能有效避免碰撞评率 是经过高大上的算法推演而来
    private static final int HASH_INCREMENT = 0x61c88647;
    private static AtomicInteger nextHashCode = new AtomicInteger();
    // 用来计算table下标的hash算法中hash值 index = hash值 & table.length - 1
    // 看来用的还是最接地气的hash算法啊 不用费脑细胞
    private final int threadLocalHashCode = nextHashCode.getAndAdd(HASH_INCREMENT);
    static class ThreadLocalMap {
        private Entry[] table;
        static class Entry extends WeakReference> {
            
            Object value;

            Entry(ThreadLocal k, Object v) {
               // key是threadLocal 当threadLocal的强引用都断掉时 WeakRefrence中的key将被回收
               // 那value要咋获取吗 这是个问题 难道不能通过hash算法算出index后 返回table[index].value
               // 如果这样干了 那发生hash碰撞的元素该怎么办 毕竟不是桶 默认循环数组放在下个为null的空间中
                super(k);
                value = v;
            }
        }
    }
}

进过上面分析我们知道get方法原理就是 thread.threadLocals.getEntry(threadLocal) 那下面我们来分析getEntry方法

 private Entry getEntry(ThreadLocal key) {
     // 通过hash算法获取下标
     // 如table.lenth = 16  hash值为0、1、3、15、16、30、60下标分别是 0、1、3、0、1、0、0
     // 即hash值为1和16的元素都想要1这个坑 即发生了hash碰撞 hashMap中hash把值相同的放到一个链表或红黑树中
     // 如下标为1这个坑被1占用了 那16只能往后延 占用下标为2的坑 如下标2到15的坑都被占用了 占用下标为0的坑 
     int i = key.threadLocalHashCode & (table.length - 1);
     Entry e = table[i];
     // entry不为null 且entry的key == threadLocal  e.get() 获取的是entry中的key即threadLocal
     if (e != null && e.get() == key)
         return e;
     else
         // 获取的同时 到下一个空槽前 进行清洗和下标的检查与尽力修复
         // 提前获取到提前返回 获取不到下一个空槽返回null
         return getEntryAfterMiss(key, i, e);
 }
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    // 扩不扩容我不管 我先确定长度 
    int len = tab.length;

    // 循环遍历数组中的元素 当元素即entry为null时停止
    // 我担心过可能会由其他线程把为null的坑赋值导致死循环问题
    // 但后来一想 这是thread对象的threadLocals 始终只有一个线程访问 不会出现竞争问题
    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            // 遍历数组中的元素时 碰到key相等的返回
            return e;
        if (k == null)
            // 当前位置到下一个空槽之间 进行清洗垃圾 修复元素索引  
            // 从当前下标开始循环清洗threadLocal被回收的entrys 
            // 当碰到entry为null时停下来 清洗的目的把已经回收的threadLocal对应的entrys置为null 让gc进行回收
            // 当检查到key为null时 它相邻的entry中的key也很可能为null
            expungeStaleEntry(i);
        else
            // 循环获取下一个元素的下标
            i = nextIndex(i, len);
       // 待处理的元素
        e = tab[i];
    }
    return null;
}
// staleSlot 据有空键的插槽 陈旧的插槽 
private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot  删除staleSlot所在条目
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null 重新哈希直到我们遇到 null
            Entry e;
            int i;
    	    // 循环检查到下一个空槽 key为null的清理掉 不为null 检查且下标 尽力修复它到合理的位置
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                // 这里才是for循环体哦
                ThreadLocal k = e.get();
                if (k == null) {
                    // threadLocal不可达 既已置为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.
                        // 扫描到空槽才停止 把e安排进去  第一个h才是它应该呆的地方 后续的占用别人的槽
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            // 这是个空槽下标 将被返回 
            // getEntryAfterMiss和remove方法会无视该返回值
            return i;
        }
remove
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) {
            // 把weekRef中的key置为null
            e.clear();
            // 进行一把元素清理 下标修复
            expungeStaleEntry(i);
            return;
        }
    }
}
set
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 比get和remove清理的更深 平衡效果和时间
        map.set(this, value);
    else
        createMap(t, value);
}
 
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.

    // 我们不像 get() 那样使用快速路径,
    // 因为使用 set() 创建新条目至少与替换现有条目一样常见,
    // 在这种情况下,快速路径通常会失败.

    Entry[] tab = table;
    int len = tab.length;
    // 计算下标
    int i = key.threadLocalHashCode & (len-1);

    // 老操作 便利到下一个空槽 替换entry中的value
    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) {
            // 为什么不直接赋值 因为可能对应的entry已存在 只是被往后安排了
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 没找到entry也没有陈旧entry 新增个entry
    tab[i] = new Entry(key, value);
    // 元素数量递增
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 当没有清洗出元素 或 数量大于等于数组的3分之二 将重新散列
        rehash();
}
private void replaceStaleEntry(ThreadLocal key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    // 备份以检查当前运行中先前的陈旧条目。我们一次清除整个运行(陈旧到下一个空槽之间的一系列条目),
    // 以避免由于垃圾收集器成束地释放引用(即,每当收集器运行时)而导致的持续增量重新散列。
    // 分批清除垃圾 分配回收

    // staleSlot.hasPrevStaleSlot ? staleSlot.prevStaleSlot : staleSlot.staleSlot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)){
        if (e.get() == null)
            slotToExpunge = i;
    }

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        // 如果我们找到键,那么我们需要将它与陈旧条目交换以维护哈希表顺序。
        // 然后可以将新的陈旧槽或在其上方遇到的任何其他陈旧槽发送到 expungeStaleEntry
        // 以删除或重新散列运行中的所有其他条目(陈旧到下一个空槽之间修正所有该修正的下标)。
        if (k == key) {
            // 从陈旧往下找 找到了key相同的元素 既该元素存储在它不应该存储的地方 应该进行下标修复
            e.value = value;

            // 下标修复 跟陈旧交换位置
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot){
                // 如果要清理的槽位等于staleSlot 由于交换操作后该槽位已经修复了
                // 存放了它该存放的元素  赋值后i成了陈旧 所以它将被清除
                slotToExpunge = i;
            }
            // 清除陈旧和修复下标 成就和往后安排 认为会成堆出现
            // cleanSomeSlots时间和效率平衡清理后续陈旧槽位
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            // 如果待清理的下标不是hash下表时 开始从待清理下标处开始清理
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 清除value 赋新值
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        // 如果有多个空槽存在 开始从清理空槽开始清理
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
 *
 * 找到陈旧元素 从该下标开始循环清理修复下标到下一个空槽 为n赋值数组的长度 接着空槽开始继续往下找
 * 没找到n除以2 往下找 直到n为0为止 平衡扫描时间和扫描效果 该思想 可借鉴
 * 找到了我就接着往下继续扫描整个数组 没找到把扫描距离缩短到先前的一办 有点二分的味道啊
 */
private boolean cleanSomeSlots(int i, int n) {
     boolean removed = false;
     Entry[] tab = table;
     int len = tab.length;
     do {
         i = nextIndex(i, len);
         Entry e = tab[i];
         if (e != null && e.get() == null) {
             n = len;
             removed = true;
             // 准空槽到真实空槽 元素清除和下标修复
             // 接着返回值继续开始 既处理的不在处理
             i = expungeStaleEntry(i);
         }
         // (n = n / 2) != 0
     } while ( (n >>>= 1) != 0);
     return removed;
 }
private void rehash() {
    // 清除陈旧和重新散列
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    //  (len * 2 / 3) - ( len * 2 / 3 / 4) 使用一半将扩容 比HashMap 0.75低一半啊
    if (size >= threshold - threshold / 4)
        resize();
}
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            // 为什么不把结果赋值给j是为了 修复下标 既重新散列 和防止死循环 
            // 如赋值下标j 
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                // 存在往后安排
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}
问题

通过get remove 和set的优化 能让泄漏尽可能的少 但是不会杜绝 因为前提条件它要threadLocal置为null 如果threadLocal还持有 但后续长时间没使用该threadLocal 而里面存放的数据允许回收而得不到回收 当threadLocal置为null 也不一定能清洗到

建议

无需缓存的数据 使用完因及时remove 缓存的大对象 需要考量是否一定需要缓存 如需要缓存因考虑使用SoftReference和WeakReference 起码能做到大对象在内存吃紧或gc时能被回收

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/530999.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号