广告
返回顶部
首页 > 资讯 > 后端开发 > Python >ThreadLocal数据存储结构原理解析
  • 406
分享到

ThreadLocal数据存储结构原理解析

2024-04-02 19:04:59 406人浏览 安东尼

Python 官方文档:入门教程 => 点击学习

摘要

目录一:简述二:TheadLocal的原理分析1.ThreadLocal的存储结构2.源码分析set()方法三:源码分析createMap()源码:流程图:expungeStaleE

一:简述

我们很多时候为了实现数据在线程级别下的隔离,会使用到ThreadLocal,那么TheadLocal是如何实现数据隔离的呢?今天就和大家一起分析一下ThreadLocal的实现原理。

二:TheadLocal的原理分析

1.ThreadLocal的存储结构

每个Thread对象中都有一个threadLocals成员变量,threadLocals是一个类型为ThreadLocalMap的map,而ThreadLocal正是基于这个map来实现线程级别的数据隔离的。

我们先看ThreadLocalMap的成员变量

        //默认的初始化容量大小
        private static final int INITIAL_CAPACITY = 16;
        //Entry数组 真正存储的数据结构
        private Entry[] table;
        //记录当前元素的数量
        private int size = 0;
        //扩容的阈值
        private int threshold;

Entry数组是真正存储数据的地方,可以看出Entry是一个key-value的存储结构,以当前ThreadLocal对象的引用作为key,存储的值为value。Entry继承了WeakReference,并且在构造函数的时候,调用super(k)(也就是WeakReference的构造函数)来对key进行初始化,所以Entry的key是一个弱引用。

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

根据上面的分析,我们可以知道ThreadLocal的存储结构大概是这样的:

2.源码分析

接下来我们从ThreadLocal的set(),get(),remove()方法为入口对ThreadLocal的源码进行分析。

set()方法

首先判断当前线程的threadLocals是否初始化,如果没有初始化,那么调用createMap()方法进行初始化并设置值,否则调用ThreadLocalMap的set()方法设置值。

流程图:

三:源码分析

public void set(T value) {
         //利用当前线程获取它的threadLocals(threadLocals是一个ThreadLocalMap)
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //如果已经初始化 那么就调用ThreadLocalMap的set()方法
        if (map != null)
            map.set(this, value);
        else
            // 没有初始化 先进行初始化
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        //返回当前线程的threadLocals
        return t.threadLocals;
    }

createMap()

createMap()会调用ThreadLocalMap的构造函数对当前线程的threadLocals初始化,并且初始化Entry数组,然后利用hash算法计算出数组下标,将需要set的值存储在Entry数组。

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
        ThreadLocalMap(ThreadLocal<?> firsTKEy, Object firstValue) {
            // 初始化Entry数组
            table = new Entry[INITIAL_CAPACITY];
            //计算数组下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //设置默认的扩容阈值 和默认容量一样
            setThreshold(INITIAL_CAPACITY);
        }

如果threadLocals已经初始化,直接调用ThreadLocalMap的set(),接下来看ThreadLocalMap的set()方法。

首先利用hash算法计算数组下标,如果计算出的位置没有值,直接将值设置进去,如果存在值(出现hash冲突),分为三种情况:

1.如果key相同,那么直接覆盖值

2.如果计算出的位置的Entry的key为null,那么说明是无效的数据(key为null,entry不为null),为了避免内存泄漏需要清除这种数据。所以调用replaceStaleEntry()方法将无效数据清除并且将需要设置的值设置到Entry数组中。

3.如果key不相同,而且计算出的位置的Entry的key不为null,那么进入到下一次for循环将计算出的下标+1,(如果到达下标最大值,则设置为0),利用新的位置重新进行判断,直到获取到一个合法的位置(线性寻址法解决hash冲突的问题)。

注:这里大家可以评论区讨论下为什么不和HashMap那样利用链表法解决hash冲突。我个人的看法是因为ThreadLocal的数据量不会向HashMap那么多,所以不需要利用链表和红黑树来解决hash冲突,链表法解决代码相对比较复杂而且扩容迁移数据的数据会比较麻烦。

源码:

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);
            //如果出现hash冲突会进入for循环
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //如果key相同 那么直接将值覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果key为null 那么说明是无效的数据 需要进行清除
                if (k == null) {
                    //调用replaceStaleEntry()方法进行清除数据 并设置值
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //如果没有hash冲突 直接赋值到对应下标的位置
            tab[i] = new Entry(key, value);
            // 将当前元素个数+1
            int sz = ++size;
            //如果没有需要清除的元素,并且当前元素个数已经达到扩容的阈值,那么进行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

接下来看replaceStaleEntry(),看ThreadLocal是如何清除无效的数据的。

当前节点是无效的数据,那么周围也可能存在无效的数据,所以ThreadLocal在清除无效的数据时,会顺便清除周围的连续的无效数据,先利用for循环从当前节点向前遍历,调整slotToExpunge的值(slotToExpunge 用于保存开始清除无效数据的下标位置), 然后向后遍历,如果有entry的key和需要存放的数据的key相同,那么直接覆盖值,并且交换当前节点和新设置的entry的值。

流程图:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
	    // slotToExpunge 用于保存开始清除无效数据的下标位置
            int slotToExpunge = staleSlot;
	    //从当前位置向前遍历,直到找到一个有效数据的下标
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
		//e.get()返回Entry的key,威null代表是无效数据 
                //(因为只有entry不为null才会进入for循环) 所以key为null,就是无效数据
                //for循环将清除无效数据的下标往前挪
                if (e.get() == null)
                    slotToExpunge = i;
            //从当前位置往后遍历
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                //如果遍历的是否发现有和当前Entry相同的key的entry,那么交换两者的位置
                if (k == key) {
                    e.value = value;
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    //如果slotToExpunge和staleSlot相等 
                    //证明当前节点的前面没有和当前节点连续的无效数据 
                    //所以从交换完的位置开始清除无效数据 调用cleanSomeSlots()方法和expungeStaleEntry()方法清除无效数据 清除完返回。
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
                //如果key为null 而且当前entry之前没有与当前节点连续的无效数据
                //刷新开始清除无效数据的下标
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
            // If key not found, put new entry in stale slot
            //如果没有找到连续的无效数据 把当前的节点的value重置为null 并且将新的值赋值到当前位置
            //因为当前的entry是无效的数据 
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);
            // If there are any other stale entries in run, expunge them
            //如果slotToExpunge 和 staleSlot不相等 说明有连续的无效数据需要顺便清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

注:大家可以在评论区讨论一下,这里为什么要交换一下数据,我个人认为,第一是为了保证数据存储的位置尽可能的在hash计算出位置,有利于后续的get()方法,第二:交换位置之后有利于让无效的数据连续起来,提高清除无效数据的效率。

真正清除无效数据的方法是expungeStaleEntry()方法和cleanSomeSlots()方法

我们先看expungeStaleEntry()方法

expungeStaleEntry()

expungeStaleEntry()方法从当前节点开始向后遍历(直到遇到enrty为null的节点),将无效数据清除,并且重新计算有效的entry的数组下标,如果计算出的下标和entry的下标不相同(这是因为采用了线性寻址法,所以hash计算出下标可能和实际的下标不一样),重新找到合适的位置。

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) {
                    //key为null 证明是无效数据 清除
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //重新计算数组下标 如果数组下标发生变化 那么将数据迁移到新的位置上
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
                        //重新利用线性寻址法寻找合适的下标位置
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

然后是cleanSomeSlots()方法

cleanSomeSlots()

调用log(n)次expungeStaleEntry()方法进行清除无效数据。这个官方说不调用n次来清除,为了效率,而且经过测试调用log(n)次清除无效的数据的效果已经很好了。(n代表entry数组的长度)。

private boolean cleanSomeSlots(int i, int n) {
            //removed 是否清除了数据的标记
            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);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

如果set()方法设置值之后,需要扩容会调用rehash()方法进行扩容。

先调用expungeStaleEntries()清除一下数据,如果还是需要扩容,那么调用resize()进行扩容。

rehash()

       private void rehash() {
            //再试清除一下数据
            expungeStaleEntries();
            // Use lower threshold for doubling to avoid hysteresis
            //如果还是需要扩容 那么会调用 resize()进行扩容
            if (size >= threshold - threshold / 4)
                resize();
        }

resize()

resize()方法会创建一个容量为原来两倍的数组,并且将数据迁移到新的数组上面,将新的数组赋值给table变量。(扩容方法比较简单)

        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);
                        //线性寻址法解决hash冲突
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

get()方法

获取到当前线程的threadLocals,如果threadLocals已经初始化,那么调用getEntry()方法获取值。否则调用setInitialValue()获取我们在initialValue()设置的初始化的值。

public T get() {
        Thread t = Thread.currentThread();
        //利用当前线程获取它的threadLocals(threadLocals是一个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();
    }

现在我们看getEntry()方法

如果找到key相同的Entry 直接返回,否则调用getEntryAfterMiss()方法

getEntry()

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果找到key相同的Entry 直接返回
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

getEntryAfterMiss()

getEntryAfterMiss()从当前节点往后遍历查找,遍历找到key相同的entry,找到就返回,否则返回null,如果有无效数据,顺便清除一下。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            while (e != null) {
                ThreadLocal<?> k = e.get();
                //找到key相同的entry 直接返回
                if (k == key)
                    return e;
                if (k == null)
                    //当前数据为无效数据 清除一下
                    expungeStaleEntry(i);
                else
                    //否则向后继续查找
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
}

最后是remove()方法

remove()

利用hash算法计算下标,从下标位置开始往后遍历,找到key相同的entry,将entry删除,顺便调用expungeStaleEntry()方法清除一下无效的数据。

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();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

四:总结

本篇文章对ThreadLocal的数据存储结构,以及set(),get(),remove()方法进行了分析。最后给大家可以再讨论一个问题:为什么ThreadLocal的Entry的key要使用弱引用?

以上就是ThreadLocal数据存储结构原理解析的详细内容,更多关于ThreadLocal数据存储结构的资料请关注编程网其它相关文章!

--结束END--

本文标题: ThreadLocal数据存储结构原理解析

本文链接: https://www.lsjlt.com/news/169021.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • ThreadLocal数据存储结构原理解析
    目录一:简述二:TheadLocal的原理分析1.ThreadLocal的存储结构2.源码分析set()方法三:源码分析createMap()源码:流程图:expungeStaleE...
    99+
    2022-11-13
  • 浅析理解Oracle数据库体系结构和存储结构
    一、Oracle体系结构 个人比喻帮助理解:类似于图书馆,去图书馆的客户(用户进程和服务进程等)需要调取资料,求助于图书管理员(实例)进入图书分区(数据库)进行资料查找。【如果比喻不当,欢迎指正,尽请谅...
    99+
    2022-10-18
  • Java 数据结构线性表之顺序存储详解原理
    目录线性表的定义线性表的基本运算线性表的存储之顺序存储定义线性表添加元素查找元素删除元素打印线性表实现的完整代码测试一下线性表的定义 线性表的逻辑特征: ①有且仅有一个称为...
    99+
    2022-11-12
  • Python数据结构之图的存储结构详解
    一、图的定义 图是一种比树更复杂的一种数据结构,在图结构中,结点之间的关系是任意的,任意两个元素之间都可能相关,因此,它的应用极广。图中的数据元素通常被称为顶点 ( V e r t ...
    99+
    2022-11-12
  • PHP数据结构之图存储结构的示例分析
    这篇文章主要介绍PHP数据结构之图存储结构的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!图的存储结构图的概念介绍得差不多了,大家可以消化消化再继续学习后面的内容。如果没有什么问题的话,我们就继续学习接下来的...
    99+
    2023-06-20
  • ConcurrentHashMap 存储结构源码解析
    目录引言1 ConcurrentHashMap 1.71.存储结构2. 初始化3. put4. 扩容 rehash5. get2 ConcurrentHashMap 1.81. 存储...
    99+
    2022-11-13
    ConcurrentHashMap 存储结构 ConcurrentHashMap 存储
  • Redis数据结构SortedSet的底层原理解析
    目录概述一些常用命令实现跳跃表跳表的插入压缩列表概述 一些常用命令 存储:zadd key score value获取:zrange key start end获取:同时获取分数:zrange key start end...
    99+
    2022-07-13
    Redis数据结构底层原理 SortedSet底层原理 Redis数据结构SortedSet
  • PostgreSQL数据库体系结构-存储结构
    PostgreSQL数据库体系结构-存储结构 数据库聚簇逻辑结构(Logical Structure of Database Cluster) database cluster--数据库聚簇,是一组数据库的集合,而不是多个数据库服务器 ...
    99+
    2021-08-02
    PostgreSQL数据库体系结构-存储结构
  • Go反射底层原理及数据结构解析
    目录1. 反射的引入与介绍2. 反射的数据结构3. 如何通过反射对象来修改原数据对象的值?1. 反射的引入与介绍 在计算机科学中,反射是指计算机程序在运行时(Run time)可以访...
    99+
    2022-11-13
  • Oracle 数据库 体系结构(一):存储结构
    目录 为什么要学习体系结构? 体系结构的定义 Oracle 物理结构 Oracle 逻辑结构 总结 为什么要学习体系结构? 之前的文章有讲解到 MySQL 、MongoDB 数据库,这些数据库我...
    99+
    2022-10-18
  • ThreadLocal作用原理与内存泄露示例解析
    目录ThreadLocal作用简单例子局部变量、成员变量 、 ThreadLocal、静态变量共享 or 隔离原理源码分析TheadLocalTheadLocalMapThreadL...
    99+
    2022-11-13
  • 如何理解大数据时代的结构化存储数据库HBase
    本篇文章为大家展示了如何理解大数据时代的结构化存储数据库HBase,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。Hbase非常适合于非结构化数据存储的数据库,200...
    99+
    2022-10-19
  • 图解vsan存储结构/数据恢复方法
    VSAN是一种以vSphere内核为基础进行开发、可扩展的分布式存储架构。VSAN通过在vSphere集群主机当中安装闪存和硬盘来构建VSAN存储层,由VSAN进行控制和管理,形成一个供vSphere集群使用的统一共享存储层。vSphere...
    99+
    2023-06-04
  • 如何理解C语言数据结构中线性表的链式存储结构
    如何理解C语言数据结构中线性表的链式存储结构,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。1.什么是线性表的链式存储结构 —链表存储结点:包括元素本身的信息,还有元素之间的关系...
    99+
    2023-06-21
  • 怎样理解Linux的存储结构
    怎样理解Linux的存储结构,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。什么是目录 Windows下管C:\,D:\,E:\,F:\ 都是根目录而在linux...
    99+
    2023-06-16
  • MySQL 数据库的存储结构 - G
    MySQL 数据库的存储结构   数据库存储结构 从小到大、行>页 >区>段>表空间 (在Oracle中将页称为"块") 页是数据库管理存储空间的基本单位,即,数据库I/O的最小单位是页 InnoDB默认页大小为16K,可以通过...
    99+
    2018-03-13
    MySQL 数据库的存储结构 - G
  • mongodb数据存储结构是什么
    MongoDB的数据存储结构是基于文档模型的,它使用了一种称为BSON(Binary JSON)的二进制编码格式来表示和存储文档数据...
    99+
    2023-09-12
    mongodb
  • 怎么理解MySQL InnoDB的存储结构
    这篇文章将为大家详细讲解有关怎么理解MySQL InnoDB的存储结构,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。 从物理意...
    99+
    2022-10-19
  • SQL Server数据存储结构是什么
    这期内容当中小编将会给大家带来有关SQL Server数据存储结构是什么,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。文件类型  数据库在磁盘上是以文件为单位存储的,由数...
    99+
    2022-10-18
  • redis怎么存储结构化数据库
    Redis是一个键值存储系统,它并不是一个结构化数据库,但是可以使用一些技巧来存储结构化数据。1. 使用Hash数据结构:可以将结构...
    99+
    2023-09-05
    redis 数据库
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作