《ThreadLocal使用与学习总结:2023-12-15》由浅入深全面解析ThreadLocal

news2024/11/28 14:41:06

由浅入深全面解析ThreadLocal

目录

  • 由浅入深全面解析ThreadLocal
    • 简介
    • 基本使用
    • ThreadLocal与synchronized的区别
    • ThreadLocal现在的设计(JDK1.8)
    • ThreadLocal核心方法源码分析
    • ThreadLocalMap源码分析
    • 弱引用与内存泄露(内存泄漏和弱引用没有直接关系)
    • ThreadLocal核心源码(Hash冲突解决)

简介

  1. 线程并发:在多线程并发的场景下使用
  2. 传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  3. 线程隔离:每个线程的变量都是独立的,不会相互影响

基本使用

  1. 常用方法
    在这里插入图片描述

  2. 代码案例实现
    (1) 不使用ThreadLocal时模拟多线程存取数据

public class ThreadLocalDemo1 {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        ThreadLocalDemo1 threadLocalDemo = new ThreadLocalDemo1();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    /**
                     * 每一个线程存一个变量,过一会取出这个变量
                     */
                    threadLocalDemo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("------------------------");
                    System.out.println(Thread.currentThread().getName() + "----->" + threadLocalDemo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

结果:

------------------------
线程0----->线程4的数据
------------------------
线程4----->线程4的数据
------------------------
线程2----->线程4的数据
------------------------
线程3----->线程4的数据
------------------------
线程1----->线程4的数据

(2) 使用ThreadLocal对多线程进行数据隔离,把数据绑定到ThreadLocal
(传统解决方案首先想到的就是加锁,确实可以实现,但是却牺牲了效率,需要等待上一个线程之行结束才可以往下之行)

public class ThreadLocalDemo2 {
    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private String content;

    public String getContent() {
        return threadLocal.get();
    }

    public void setContent(String content) {
        threadLocal.set(content);
    }

    public static void main(String[] args) {
        ThreadLocalDemo2 threadLocalDemo2 = new ThreadLocalDemo2();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    /**
                     * 每一个线程存一个变量,过一会取出这个变量
                     */
                    threadLocalDemo2.setContent(Thread.currentThread().getName()+"的数据");
                    System.out.println("------------------------");
                    System.out.println(Thread.currentThread().getName() + "----->" + threadLocalDemo2.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

结果:

------------------------
------------------------
------------------------
线程3----->线程3的数据
------------------------
线程2----->线程2的数据
线程1----->线程1的数据
线程0----->线程0的数据
------------------------
线程4----->线程4的数据

ThreadLocal与synchronized的区别

二者都是用来处理多线程并发访问的问题,但是二者的原理和侧重点不一样,简要说就是,ThreadLocal牺牲了空间,而synchronized是牺牲了时间来保证线程安全(隔离)。
在这里插入图片描述
总结:在上述的案例当中,使用ThreadLocal更为合理,这样保证了程序拥有了更高的并发性。

ThreadLocal现在的设计(JDK1.8)

  1. 简介
    每一个Thread维护一个ThreadLocalMap,这个Map的key为ThreadLocal实例本身,而value则为实际存储的值。
  2. 具体过程
    (1)每一个Thread内部都有一个Map(ThreadLocalMap)
    (2)Map里面存储的ThreadLocal对象(key)和线程的变量副本(value)
    (3)Thread的Map是由ThreadLocal来维护的,由ThreadLocal负责向Map获取和设置线程的变量值。
    (4)对于线程获取值,每一个副本只能获取当前线程本地的副本值,别的线程无法访问到,互不干扰,实现了线程隔离。
  3. 对比与1.8之前的设计(相当于Thread与ThreadLocal的角色互换了)
    在这里插入图片描述
  4. 1.8设计的好处
    (1)每个Map存储的Entry数量变少了(因为实际状况下Thread比ThreadLocal多)
    (2)当Thread销毁时,ThreadLocalMap也会随之销毁,避免内存的浪费

ThreadLocal核心方法源码分析

在这里插入图片描述

  1. get方法源码
public T get() {
		// 获取当前线程
        Thread t = Thread.currentThread();
        // 获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // map不为空时,获取里面的Entry
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 返回结果
                return result;
            }
        }
        // 没有则赋值初始值null并返回
        return setInitialValue();
    }
  1. set方法源码
public void set(T value) {
		// 获取当前线程
        Thread t = Thread.currentThread();
        // 获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	// 不为空直接set
            map.set(this, value);
        } else {
        	// map为空则创建并set
            createMap(t, value);
        }
    }
  1. initialValue方法返回初始值(protected修饰为了让子类覆盖设计的)需要自定义初始值可以重写该方法
protected T initialValue() {
        return null;
    }
  1. remove方法
public void remove() {
		 // 获取当前线程的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
         	// 移除ThreadLocalMap
             m.remove(this);
         }
     }
  1. setInitialValue方法
private T setInitialValue() {
		// 得到初始化值null
        T value = initialValue();
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程中ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // map存在的话把null设置进去,不存在则创建一个并将null设置进去
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        // 如果当前ThreadLocal属于TerminatingThreadLocal(关闭的ThreadLocal)则register(注册)到TerminatingThreadLocal
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

ThreadLocalMap源码分析

  1. 简介
    ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,是独自设计实现Map功能,内部的Entry也是独立的。
  2. 结构图解
    在这里插入图片描述
  3. 成员变量
		// Entry类,继承弱应用,为了和Thread的生命周期解绑
		static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         * 初始容量,必须是二的幂
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         * 根据需要调整大小。长度必须是2的幂。
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         * table中的entrie数量
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         * 要调整大小的下一个大小值
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         * 设置调整大小阈值以维持最坏的2/3负载因子
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         * 增量一
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         * 减量一
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

弱引用与内存泄露(内存泄漏和弱引用没有直接关系)

  1. 内存泄漏/溢出概念
    (1)Memory overflow:内存溢出,没有足够的空间提供给申请者使用
    (2)Memory leak:内存泄漏,系统中已动态分配的堆内存由于某种原因无法释放或者没有释放,导致系统内存堆积,影响系统运行,甚至导致系统崩溃。内存泄漏终将导致内存溢出。
  2. 强/弱引用概念
    (1)Strong Referce:强引用,我们常见的对象引用,只要有一个强引用指向对象,也就表明还“活着”,这种状况下垃圾回收机制(GC)是不会回收的。
    (2)Weak Referce:弱引用,继承了WeakReferce的对象,垃圾回收器发现了只具有弱引用的对象,不管当前系统的内存是否充足,都会回收他的内存。
  3. 如果key,即Entry使用强引用,也无法避免内存泄漏
    因为Entry是在Thread当前线程中,生命周期和Thread一样,没有手动删除Entry时Entry就会内存泄漏。
  4. 也就是说,只要在调用完ThreadLocal后及时使用remove方法,才能避免内存泄漏
    在这里插入图片描述
    在这里插入图片描述

ThreadLocal核心源码(Hash冲突解决)

  1. 从构造方法入手

		ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
			// 初始化table
            table = new Entry[INITIAL_CAPACITY];
            // 计算索引在数组中的位置(核心代码)
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 设置值
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 设置阈值(INITIAL_CAPACITY的三分之二)
            setThreshold(INITIAL_CAPACITY);
        }
  1. 重点分析int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

(1)这里定义了一个AtomicInteger,每次获取并加上HASH_INCREMENT(0x61c88647,这个值与斐波那契数(黄金分割)有关),是为了让哈希码能够均匀的分布在2的n次方的数组(Entry[])里面,也就尽可能避免了哈希冲突。
(2)hashcode & (INITIAL_CAPACITY - 1) 相当于hashcode % (INITIAL_CAPACITY - 1) 的高效写法,所以size必须为2的次幂,这样最大程度避免了哈希冲突。

  1. set方法源码分析
private void set(ThreadLocal<?> key, Object value) {
    		
            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)]) {
                 // 获取到该Entry对应的ThreadLocal
                ThreadLocal<?> k = e.get();
                if (k == key) {
                // key存在则覆盖value
                    e.value = value;
                    return;
                }
                // key为null但是值不为null,这说明了之前使用过,但是ThreadLocal被垃圾回收了,当前的Entry是一个陈旧的(Stale)元素
                if (k == null) {
                // key(ThreadLocal)不存在,则新Entry替换旧的Entry,此方法做了不少垃圾清理的动作,避免了内存泄漏。
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // ThreadLocal中未找到key也没有陈旧的元素,此时则在这个位置新创建一个Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // cleanSomeSlots用于清理e.get()为null的key,如果大于阈值(2/3容量)则rehash(执行一次全表扫描清理工作)
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
		/**
		* 线性探测法查找元素,到最后一个时重定位到第一个
		*/
		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

(详细视频可前往B站黑马程序员)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1318557.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第十四天 | 二叉树理论基础、递归遍历 、迭代遍历、统一迭代

今天学习内容&#xff1a;二叉树理论基础、递归遍历 、迭代遍历、统一迭代 讲解&#xff1a;代码随想录 二叉树题目思维导图如下&#xff0c;来自代码随想录。 1.二叉树理论基础 1.1二叉树种类 满二叉树 完全二叉树 二叉搜索树 平衡二叉搜索树 C中map、set、multimap&…

[Verilog] Verilog 操作符与表达式

主页&#xff1a; 元存储博客 文章目录 前言1. 操作符2. 操作数3 表达式总结 前言 1. 操作符 图片来源&#xff1a; https://www.runoob.com/ Verilog语言中使用的操作符包括&#xff1a; 算术操作符&#xff1a;加法()、减法(-)、乘法(*)、除法(/)、取模(%)、自增()、自减(–…

Vue中插槽的使用

目录 一、默认插槽 &#xff08;1&#xff09;概念 &#xff08;2&#xff09;代码展示 &#xff08;3&#xff09;后备内容 二、具名插槽 &#xff08;1&#xff09;概念 &#xff08;2&#xff09;代码展示 三、作用域插槽 &#xff08;1&#xff09;概念 &#xff0…

【经典LeetCode算法题目专栏分类】【第2期】组合与排列问题系列

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 组合总和1 class So…

【计算机组成原理】存储系统基本概念与基本组成

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

FRP内网映射家用服务器至公网访问

兄弟们&#xff0c;服务器到货了&#xff0c;后续与大家分享内容就用它了。我预装的操作系统是Centos8,首先要解决的是远程访问的问题。 【特别注意】下述的端口&#xff0c;记得在阿里云安全组配置中放开端口入规则&#xff01;&#xff01; 1. FRP服务器配置 之前我有购买…

UDP多人聊天室

讲解的是TCP和UDP两种通信方式它们都有着自己的优点和缺点 这两种通讯方式不通的地方就是TCP是一对一通信 UDP是一对多的通信方式 UDP通信 主要的方向是一对多通信方式 UDP通信就是一下子可以通信多个对象&#xff0c;这就是UDP对比TCP的优势&#xff0c;UDP它的原理呢 就是…

Spring之容器:IOC(1)

学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持&#xff0c;想组团高效学习… 想写博客但无从下手&#xff0c;急需…

破译模式:模式识别在计算机视觉中的作用

一、介绍 在当代数字领域&#xff0c;计算机视觉中的模式识别是关键的基石&#xff0c;推动着众多技术进步和应用。本文探讨了计算机视觉中模式识别的本质、方法、应用、挑战和未来趋势。通过使机器能够识别和解释视觉数据中的模式&#xff0c;模式识别不仅推动了计算机视觉领域…

什么是回调函数

需求 A&#xff0c;B两个小组开发一个功能。B小组开发制作油条模块:make_youtiao。A小组需要调用B小组开发的模块&#xff0c;然后执行后续的操作&#xff1a;sell()如下图&#xff1a; 上面的方式A小组必须等待B小组开发的模块make_youtiao执行完成后才能执行sell()。 上图代…

JVM-2-对象

对象创建 当Java虚拟机遇到一条字节码new指令时&#xff0c;首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用&#xff0c;并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有&#xff0c;那必须先执行相应的类加载过程。 为对象分配空间…

linux性能优化-cpu使用率

文章目录 1.CPU使用率2.节拍率的概念2.1.查看系统节拍率2.2.用户节拍率2.3.CPU使用率公式 3.怎么查看CPU使用率3.1.top显示系统总体CPU使用情况3.2.pidstat分析每个进程CPU使用情况 4.CPU使用率过高怎么办4.1.perf命令详解 1.CPU使用率 用什么指标来描述系统的CPU性能呢?不是…

SpringData JPA 整合Springboot

1.导入依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0…

Oracle 透明网关安装

Oracle 11g透明网关连接Sqlserver oracle 透明网关是oracle连接异构数据库提供的一种技术。通过Gateway&#xff0c;可以在Oracle里透明的访问其他不同的数据库&#xff0c;如SQL Server, DB2, Sybase等等&#xff0c;就像远程Oracle数据库一样。配置后的sql查询的处理流程&…

架构设计系列之基础设施能力建设

周末聊两句&#xff1a; 今天将的基础设施能力建设部分&#xff0c;一般的架构书籍中都不存在的部分&#xff0c;这是我在实践过程中的经验和能力总结部分&#xff0c;希望和大家有一个很好的交流自从在 WeChat 中开了订阅号的两周半的时间&#xff0c;非常感谢大家的支持&…

Spring Boot 3 整合 Hutool 验证码实战

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…

中通单号查询,中通快递物流查询,对需要的单号进行备注

批量查询中通快递单号的物流信息&#xff0c;对需要的单号进行备注。 所需工具&#xff1a; 一个【快递批量查询高手】软件 中通快递单号若干 操作步骤&#xff1a; 步骤1&#xff1a;运行【快递批量查询高手】软件&#xff0c;并登录 步骤2&#xff1a;点击主界面左上角的“…

vite原理

一、依赖预构建 1、为什么需要依赖预构建 CommonJS和UMD兼容性 在开发阶段中&#xff0c;vite的开发服务器将所有的代码视为原生ES模块。因此&#xff0c;vite必须先将作为CommonJS或者UMD发布的依赖项转换为ESM。 这是vite的一个特色&#xff0c;也是为什么会相对于webpack比…

ffmpeg6.0-ffplay.c源码分析(二)之整体框架大流程分析

文章目录 main()函数解读stream_open()函数解析event_loop函数解析关注公众号看全文: 想分析任何一个可执行程序,肯定从main()函数下手是比较合适的,ffplay的源代码也是如此。 main()函数解读 /* Called from the main */ int main(int argc, char **argv)

训练属于自己的大模型LM Studio使用记录

LM Studio 支持本地运营大模型 下载地址: https://lmstudio.ai/ 一 搜索下载管理 安装之后打开, 搜索对应大模型, 下载, 举例: baichuan 点击左侧菜单栏文件夹图标进行管理 二 聊天 点击左侧菜单栏聊天图标, 新建聊天, 输入框输入内容 可能对回答的结果并不满意, 我们可以…