代码编织梦想

基本概念

在多线程编程中,线程间数据共享是非常常见的问题,但是容易出现并发问题,特别是多个线程对同一共享变量进行写入时。为了保证线程安全,一般使用者在访问成员变量时需要进行适当的同步。

我们通常会使用synchronized或者Lock加锁方式进行线程同步,但是这些方式在性能方面是有一定的损耗的。那么有没有一种方式可以在保证线程安全的前提下,又不会带来太大的性能问题呢?ThreadLocal就是这样一种机制。

在这里插入图片描述

ThreadLocal是一种线程封闭的机制,它可以将数据隔离在每个线程中每个线程都拥有一份独立的数据副本, 实际操作的是自己本地内存的变量。这样一来,不同的线程之间就不会出现数据共享的问题了。

在这里插入图片描述

使用示例

下面是一个简单的使用ThreadLocal的示例代码:

public class ThreadLocalDemo {
   	//创建ThreadLocal变量
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //创建线程
        new Thread(() -> {
            //设置threadLocal本地变量的值
            threadLocal.set("hello world");
            //使用get方法获取该值
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        }, "thread1").start();
		
        //创建线程
        new Thread(() -> {
            threadLocal.set("你好 世界");
            System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
        }, "thread2").start();
    }
}

在这个示例代码中,我们创建了两个线程,分别向ThreadLocal中设置了不同的值。由于ThreadLocal是线程封闭的机制,因此每个线程都拥有自己的独立的数据副本,两个线程之间不会互相影响。输出结果如下:

thread1 hello world
thread2 你好 世界

实现原理

ThreadLocal的实现原理其实比较简单,它主要依赖于ThreadLocalMap字段实现,其类图结构如下:

在这里插入图片描述

ThreadLocal的类结构比较简单,主要包含以下几个方法:

  • set:向当前线程的ThreadLocalMap中存储值
  • get:获取当前线程的ThreadLocalMap中的值
  • remove:清除当前线程的ThreadLocalMap中的值
  • initialValue:初始化值,可在子类中覆盖此方法以提供默认值

ThreadLocal源码:

public class ThreadLocal<T> {
    /**
     * 返回当前线程对应的ThreadLocalMap。
     * 这里的threadLocals是ThreadLocalMap类型的变量,作为存储多个ThreadLocal变量的容器,后面再具体解析
     */
    private ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * 在当前线程的ThreadLocalMap中设置值。
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //更新当前ThreadLocal变量的值,注意,这里this值ThreadLocal类的当前实例对象
            map.set(this, value);
        else
            //第一次调用就创建当前线程对应的HashMap
            createMap(t, value);
    }

    /**
     * 从当前线程的ThreadLocalMap中获取值。
     */
    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //将当前线程作为key,查找对应的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //通过this:当前实例对象获取ThreadLocal
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T) e.value;
        }
        //ThreadLocalMap为空则初始化当前线程的ThreadLocal变量值
        return setInitialValue();
    }

    /**
     * 从当前线程的ThreadLocalMap中清除值。
     */
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }

    /**
     * 初始化值。
     */
    protected T initialValue() {
        return null;
    }

    /**
     * 创建一个新的ThreadLocalMap并初始化值。
     */
    void createMap(Thread t, T firstValue) {
        //传入的key值是当前ThreadLocal的this引用
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    /**
     * 获取初始值,如果子类未提供初始值,则返回null。
     */
    T setInitialValue() {
        //初始化为空
        T value = initialValue();
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap变量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //第一次调用就创建当前线程对应的HashMap
            createMap(t, value);
        return value;
    }
}

注:一开始被ThreadLocalthreadLocalsThreadLocalMap搞晕了,后面发现其实很简单,直接把threadLocals给踢了,只剩下ThreadLocalThreadLocalMap,这样就好理解多了。threadLocals这玩意只是个变量名而已,不用关心。

记住ThreadLocal类对应一个变量,里面有一些对应的方法,而ThreadLocalMap是一个Map,里面有很多ThreadLocal, 取的时候通过当前线程和ThreadLocal对象的哈希码,找到对应的Entry对象,然后返回它的value字段。

ThreadLocal类中,主要是通过对当前线程的ThreadLocalMap的操作来实现线程本地变量的存储和获取,具体实现细节可以通过源码来了解。

ThreadLocalMap

Thread类中有一个ThreadLocalMap类型的字段threadLocals,它是线程本地变量的存储容器。每个ThreadLocal对象都有一个对应的entrythreadLocals中,用来存储线程对应的值。

ThreadLocalMap是一个自定义的HashMap,它的键为ThreadLocal对象,值为线程保存的数据对象。在每个线程中, ThreadLocalMap都是独立的,因此不同的线程之间不会出现数据共享的问题。由于ThreadLocalMap是线程独立的,因此在多线程环境下不会出现竞争的问题,从而保证了线程安全性。

ThreadLocalMap中的键值对的生命周期是跟随线程的,当线程结束后,ThreadLocalMap中的键值对也会被回收。如何线程一直不消亡,会导致这些本地变量一直存在,所以可能造成内存溢出,因此,使用结束后记得要调用ThreadLocalremove方法删除对应线程ThreadLocalMap中存储的本地变量。

ThreadLocal对象通过调用ThreadLocalset方法将值存储到当前线程的ThreadLocalMap中,通过调用ThreadLocalget方法可以获取当前线程的值。ThreadLocalremove方法可以清除当前线程中的ThreadLocalMap中的键值对,从而释放资源。

 /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

/*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
*/
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

在这里插入图片描述

Thread类中使用到两个变量:threadLocals inheritableThreadLocals ,都是 ThreadLocalMap类型的变量。

ThreadLocals线程本地变量的存储容器。每个ThreadLocal对象都有一个对应的entryThreadLocals中,用来存储线程对应的值。这里也就刚好引出另一个问题了:为什么threadLocals被设计成map结构?

很明显,ThreadLocals要作为容器存储多个ThreadLocal本地变量,并通过每个线程进行关联,自然我们就想到了map了。

ThreadLocal不支持继承性

ThreadLocal不支持继承性,也就是说,子线程无法访问父线程中的ThreadLocal变量,因为子线程中的ThreadLocal变量是子线程自己的,而不是从父线程中继承的。

举个例子,假设有一个父线程和一个子线程,父线程中有一个ThreadLocal变量,当父线程启动子线程时,子线程无法访问父线程中的ThreadLocal变量,因为子线程中的ThreadLocal变量是子线程自己的,而不是从父线程中继承的。

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("Hello, World!");

        Thread thread = new Thread(() -> {
            System.out.println("Child thread value: " + threadLocal.get());
        });
        thread.start();

        System.out.println("Main thread value: " + threadLocal.get());
    }
}

运行结果如下:

Main thread value: Hello, World!
Child thread value: null

可以看到,子线程中的ThreadLocal变量值为null,而不是从父线程中继承的值。

那有没有办法可以让子线程能够访问到父线程中的值呢?当然!

InheritableThreadLocal类

InheritableThreadLocal类是Java中的一个线程本地变量类,它继承了ThreadLocal类,扩展了ThreadLocal以提供从父线程到子线程的值继承

下面是一个使用InheritableThreadLocal类的例子:

public class InheritableThreadLocalExample {
    public static void main(String[] args) {
        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        inheritableThreadLocal.set("Hello World");

        Thread thread = new Thread(() -> {
            System.out.println(inheritableThreadLocal.get());
        });

        thread.start();
    }
}

在这个例子中,我们创建了一个InheritableThreadLocal对象,然后在主线程中设置了一个值。接着,我们创建了一个子线程,并在子线程中获取了这个值。由于InheritableThreadLocal类提供了从父线程到子线程的值继承,所以子线程中可以获取到这个值,输出结果为"Hello World"

使用场景

InheritableThreadLocal类的应用场景是当线程需要从父线程继承某些值时,可以使用InheritableThreadLocal类。

例如,当需要在多个线程之间共享用户ID或事务ID等线程本地变量时,可以使用InheritableThreadLocal类。InheritableThreadLocal提供了从父线程到子线程的值继承,因此子线程可以获取父线程中的值。

源码分析

Thread类中定义了叫inheritableThreadLocals的变量,类型为ThreadLocalMap

 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal类中继承了ThreadLocal,并重写了三个方法,替换了原有的threadLocals

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * 用于在子线程中创建一个新的InheritableThreadLocal对象时,为该对象提供一个初始值。
     * 在默认情况下,该方法返回父线程中的值,因此子线程中的InheritableThreadLocal对象的初始值与父线程中的值相同。
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * 获取的是inheritableThreadLocals变量
     * 而不再是ThreadLocals变量
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 创建当前线程的inheritableThreadLocals变量实例
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

再具体看一下childValue方法的执行流程,以及如何访问父线程的本地变量

当我们在主线程中start一个子线程时,会new 一个Thread。创建线程时发生了什么才让父子线程的InheritableThreadLocal可以传递

Thread类中,有多个默认构造方法, 经过重载,都最终调用了init方法

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
}

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
}


//最终调用
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {


    	//获取当前线程(父线程)
        Thread parent = currentThread();
       	……
    	/* 如果inheritThreadLocals为true并且父线程中存在InheritableThreadLocal对象,
    	* 则使用ThreadLocal.createInheritedMap方法创建一个新的Map对象,
    	* 该对象包含父线程中所有InheritableThreadLocal对象的值。这个新的Map对象将作为
    	* 子线程的InheritableThreadLocalMap对象的初始值。
    	*/
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

    }
inheritThreadLocals变量的值复制一份到新的//创建一个新的Map对象
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
}


private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

    		//将父线程的inheritThreadLocals变量的值复制一份到新的对象中
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        //这里调用的是InheritableThreadLocal重写后的childValue方法
                        Object value = key.childValue(e.value);//返回e.value
                        Entry c = new Entry(key, value);
                        //计算hash值
                        int h = key.threadLocalHashCode & (len - 1);
                        //找到一个空的位置添加Entry实例,就是hashmap添加元素的过程
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
}

总结

ThreadLocal是一种线程封闭的机制,它可以将数据隔离在每个线程中每个线程都拥有一份独立的数据副本

ThreadLocal主要是通过ThreadLocalMapThread类中的ThreadLocalMap字段实现,ThreadLocalMap是一个自定义的HashMap,用来存储线程本地变量的键值对,而Thread类中的ThreadLocalMap字段threadLocals则是用来存储每个线程的ThreadLocalMap

通过使用ThreadLocal,我们可以在保证线程安全的前提下,又不会带来太大的性能问题。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/x_xhuashui/article/details/129663029

ThreadLocal 解析-爱代码爱编程

使用场景 典型场景一 每个线程需要一个独享的对象 (通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random) 1000 个打印线程的任务,都用线程池来运行 避免众多对象创建和销毁的开销,把 SimpleDateFormat 提出来作为公共变量,但这就会产生线程安全问题,解决方案可以加锁,但是太影响性能了,更好的解决方

java学习笔记——集合-爱代码爱编程

目录 集合与数组的对比集合体系结构Collection——常见成员方法Collection——迭代器基本使用Collection——迭代器原理分析Collection——迭代器删除方法增强for——基本格式增强for

pthread-爱代码爱编程

问题描述: 从业务抽离出来大概是这样:大概是如下的场景,这个程序跑了40分钟后,pthread_create返回11,创建失败。 int i=0; void play_handle(){     i=0;     for(;i<5;i++){         sleep(1);         printf("i=%d\n",i);    

algorithms & data structures (m)assessed exercise-爱代码爱编程

Algorithms & Data Structures (M) Assessed Exercise (2022–23) In this exercise you will implement an ADT that represents a molecule – i.e. a group of atoms connected by bonds.

[马士兵] 一. 初识java 12.程序中常见错误-爱代码爱编程

【1】最低级的错误:单词拼写错误 【2】要求源文件名字和类名必须一模一样:  出错:  【3】所有的标点必须是英文状态下的: 中文状态:【】(){} !;:“‘《》? 英文状态:[]       ()  {}  !  ;   :   "  '  <> ? 【4】成对编程: [] {} () <> ""  '' 【

vertx入门学习(含代码)-爱代码爱编程

Vertx入门学习 一、Vertx是什么?二、Vertx基本概念三、Vertx能干什么?四、Vertx的技术体系五、快速体验:搭建一个简单的Vertx项目并输出Hello World六、单元测试总结 一、Ver

有图解有案例,我终于把 condition 的原理讲透彻了-爱代码爱编程

哈喽大家好,我是阿Q! 20张图图解ReentrantLock加锁解锁原理文章一发,便引发了大家激烈的讨论,更有小伙伴前来弹窗:平时加解锁都是直接使用Synchronized关键字来实现的,简单好用,为啥还要引用Reent

如何优雅的用poi导入excel文件-爱代码爱编程

在企业级项目开发中,要经常涉及excel文件和程序之间导入导出的业务要求,那么今天来讲一讲excel文件导入的实现。java实现对excel的操作有很多种方式,例如EasyExcel等,今天我们使用的是POI技术实现excel文件的导入。 POI技术简介 1.POI概念 Apache POI 是用Java编写的免费开源的跨平台的Java AP

jvm监控搭建-爱代码爱编程

文章目录 JVM监控搭建整体架构JolokiaTelegrafInfluxdbGrafana JVM监控搭建 整体架构 JVM 的各种内存信息,会通过 JMX 接口进行暴露。 Jolokia

mysql和mariadb,它们有什么区别?-爱代码爱编程

目录 一、MySQL简介 二、MariaDB简介 三、什么是MariaDB? 四、为什么推出MariaDB? 五、主要区别 六、总结 在这篇文章中,我们将探讨MySQL和MariaDB之间的区别。两者都是开源的关系型数据库管理系统,但两者之间有一些关键的区别。我们将介绍这两个系统的历史,它们的主要特点和性能等。 一、MySQL

【java学习笔记】34.java iterator及object 类-爱代码爱编程

前言 本章介绍Java的Iterator(迭代器)和Object 类。 Java Iterator(迭代器) Java迭代器(Iterator)是 Java 集合框架中的一种机制,它提供了一种在不暴露集合内部实现的情况

编码方式概括-爱代码爱编程

1.三种码表 1.iso8859-1码表:是一种8位的单字节编码方式。它可以表示256个字符,其中前128个字符与ASCII码相同,后面128个字符包含了一些欧洲语言中的特殊符号。iso8859-1码表在Servlet规范中被默认为字符流向浏览器发送页面信息时使用的编码方式。 2.GBK码表:是一种双字节编码方式,也称汉字内码扩展规范,是在GB23

按照树形结构直观地打印出一棵二叉树、快速创建leetcode中树的结构(java)_java打印一颗二叉树-爱代码爱编程

前言 文章最后附上的完整代码,可以放到项目里做处理TreeNode(树)结构的工具类,包括字符串转TreeNode,和可视化打印TreeNode。 平时无论是工作还是学习中,在写代码时,树总是一个非常常见的数据结构。在我

biginteger 和bigdecimal-爱代码爱编程

出处:为什么会想到大数呢?是因为最近在写leetcode刷题的时候钻牛角尖非要实现数组加1的问题导致的。(leetcode 66 加1问题)不过ps自己对大数还是用的少了,导致害怕用,然后leetcode用了还报错。(

redis常用命令-爱代码爱编程

目录 一、认识redis 1、Redis数据结构介绍 2、Redis常用命令 二、Redis基本数据类型 1、String类型 2、Hash类型 3、List类型 4、Set类型 5、SortedSet(zset)类型 关系型数据库(SQL)与非关系型数据库(NoSQL)的区别 SQLNoSQL数据结构结构化(Structur

学习笔记20230319-爱代码爱编程

目录 一、final 二、List和Set 三、HashMap扩容机制原理 四、ArrayList和LinkedList 有哪些区别 五、HashMap和HashTable区别 六、ConcurrentHashMap扩容机制原理 七、CopyOnWriteList的底层原理是怎样 八、StringBuffer、StringBuilder、S

springboot+jsp大学图书借阅管理系统前端idea_jsp图书借阅管理系统-爱代码爱编程

管理员模块 1)登录:管理员输入用户名、密码;选择“管理员”角色;点击登录按钮。 2)管理员主界面:以管理员身份登录成功后,选择进入网站系统管理模块;选择进入首页内容管理模块;选择进入图书详细管理模块;选择图书借阅管理模块。 3)网站系统管理:以管理员身份登录成功后,选择网站导航菜单管理,对导航名称进行修改删除;选择分类管理,对导航名称进行分类;选择管理员