我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java多线程之ThreadLocal浅析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java多线程之ThreadLocal浅析

介绍

什么是ThreadLocal?

ThreadLocal叫做线程变量,用于在多线程环境下创建线程本地变量。

通俗的讲,ThreadLocal可以让你在同一个线程中创建一个变量,并且这个变量对于该线程是唯一的,其他线程无法访问到这个变量。

这种方式能够有效地避免多线程之间的变量冲突问题,使得线程本地变量的访问变得更加安全和高效。

例如,在一个线程池中,每个线程需要维护自己的状态,这时就可以使用ThreadLocal来创建线程本地变量来存储状态信息。

ThreadLocal 的作用是什么?

在多线程编程中,由于不同线程之间共享内存,如果多个线程访问同一个变量,就会发生竞争条件,可能会导致数据不一致或者死锁等问题。使用ThreadLocal可以解决这个问题,因为它可以为每个线程创建一个独立的变量副本,每个线程都可以访问自己的变量副本,而不会影响其他线程的变量。这种方式可以有效地避免多线程之间的变量冲突问题,提高了程序的可靠性和性能。ThreadLocal常用于实现线程安全的单例模式,以及在多线程环境下对共享数据的缓存。

如何使用ThreadLocal

如何创建一个ThreadLocal实例

直接上代码:

public class ThreadLocalDemo {  
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();  
  
    public static void main(String[] args) {  
        new Thread(() -> {  
            System.out.println("thread1 before set: " + threadLocal.get());  
            threadLocal.set("AAAAA");  
            System.out.println("thread1 after set: " + threadLocal.get());  
            threadLocal.remove();  
            System.out.println("thread1 after remove: " + threadLocal.get());  
        }).start();  

        new Thread(() -> {  
            System.out.println("thread1 before set: " + threadLocal.get());  
            threadLocal.set("BBBBB");  
            System.out.println("thread2 after set: " + threadLocal.get());  
            threadLocal.remove();  
            System.out.println("thread2 after remove: " + threadLocal.get());  
        }).start();  

        System.out.println("main thread before set: " + threadLocal.get());  
        threadLocal.set("Main");  
        System.out.println("main after set: " + threadLocal.get());  
        threadLocal.remove();  
        System.out.println("main after remove: " + threadLocal.get());  
    }  
}

程序输出:

thread1 before set: null
main thread before set: null
main after set: Main
thread1 before set: null
thread1 after set: AAAAA
thread1 after remove: null
thread2 after set: BBBBB
thread2 after remove: null
main after remove: null

创建ThreadLocal实例的方式非常简单,只需要使用Java中的ThreadLocal类的构造函数即可。

上面的代码创建了一个ThreadLocal实例,该实例可以存储String类型的值。在使用ThreadLocal之前,需要先调用它的set()方法来初始化一个线程本地变量, 否则get()方法得到的值就是null。

从代码中可以看到, 我们在main方法中分别创建了2个线程, 三个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。

如果在当前线程中尚未设置该值或者已经调用remove()方法删除值,则返回null。

需要注意的是,每个ThreadLocal对象只能存储一个值,如果需要存储多个值,则需要创建多个ThreadLocal对象。

ThreadLocal与Synchronized的区别

ThreadLocal和Synchronized都是Java中用于处理多线程并发访问的工具,但它们的作用和实现方式有很大的区别。

作用不同:ThreadLocal主要是用来创建线程本地变量,解决多线程并发访问时的变量冲突问题;而Synchronized则是一种同步机制,用于保护共享资源,防止多线程之间的竞争条件。

  • 实现方式不同:ThreadLocal通过为每个线程创建独立的变量副本,使得每个线程之间互不干扰,从而解决多线程访问共享变量时的线程安全问题。而Synchronized则是通过互斥访问来实现同步的,即多个线程同时只能有一个线程访问共享资源。

  • 应用场景不同:ThreadLocal适用于需要在多个线程中使用独立的变量的场景,如线程池中的线程状态管理,以及Web应用中的Session管理等;而Synchronized则适用于需要保护共享资源的场景,如多个线程同时访问同一个数据结构,或者需要保证某个方法在同一时刻只能被一个线程访问等。

  • 性能影响不同:ThreadLocal相对于Synchronized来说性能更好,因为它只涉及到线程本地变量的访问和赋值操作,不需要进行锁竞争和上下文切换等操作。而Synchronized则需要进行锁竞争和上下文切换等操作,会对性能产生一定的影响。

ThreadLocal的优点:

  • 线程安全:每个线程都拥有自己的变量副本,不会受到其他线程的影响,可以避免线程安全问题。
  • 性能高:ThreadLocal使用了空间换时间的方式,每个线程都有自己的变量副本,不需要进行加锁和解锁操作,因此性能更高。
  • 代码简洁:使用ThreadLocal可以避免复杂的同步控制逻辑。

加锁的优点:

  • 保证数据一致性:通过加锁可以保证共享资源在多线程环境下的正确性,避免出现数据不一致的情况。
  • 线程同步:在加锁过程中,线程会被阻塞,等待锁的释放,保证了线程同步。

ThreadLocal的缺点:

  • 内存泄漏:ThreadLocal使用静态的内部Map来存储变量副本,如果不及时清理,会导致内存泄漏问题(后续展开介绍)。
  • 难以调试:由于每个线程都有自己的变量副本,因此在调试过程中,需要考虑多个线程的情况,会增加调试的难度。

加锁的缺点:

  • 性能问题:在高并发情况下,加锁会导致线程的阻塞,从而影响系统的性能。
  • 容易导致死锁:如果加锁的操作不正确,可能会导致死锁问题,需要谨慎使用。

综合来看,ThreadLocal适合处理线程私有的数据,而加锁适合处理共享的资源,具体应该根据业务需求来选择。

ThreadLocal的实现原理

ThreadLocal的内部数据结构

直接查看源码:

Thread类:

public  
class Thread implements Runnable {
    //MAP
    ThreadLocal.ThreadLocalMap threadLocals = null;  

    //用于父子线程变量同步, 后续介绍
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

ThreadLocal的set()方法:

    public void set(T value) {  
        //获取当前线程
        Thread t = Thread.currentThread();
        //封装方法从获取线程中的ThreadLocalMap
        //为什么封装方法呢? 为了后面扩展inheritableThreadLocals
        ThreadLocalMap map = getMap(t);  
        //之前有创建过, 直接set
        if (map != null)  
            map.set(this, value);  
        else  
            //之前没有创建, 新建Map并设置值
            createMap(t, value);  
    }

    ThreadLocalMap getMap(Thread t) {  
        return t.threadLocals;  
    }

从源码中我们可以看到, 在set方法中, 我们先是获取到当前线程, 然后以当前线程为入参调用getMap方法, 并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap, 并将value值初始化。

那么threadLocalMap又是什么呢? 我们接着往下看。

ThreadLocalMap和ThreadLocalMap.Entry的实现


public class ThreadLocal<T> {

    static class ThreadLocalMap {  
        
        //继承弱应用, 方便垃圾回收
        static class Entry extends WeakReference<ThreadLocal<?>> {  

              
            Object value;  

            Entry(ThreadLocal<?> k, Object v) {  
                super(k);  
                value = v;  
            }  
        }
        //数组, 用于存储多组数据
        private Entry[] table;
    }
}

从代码我们可以看到, threadLocalMap是ThreadLocal中的一个静态内部类, 在threadLocalMap又维护了一个名叫table的Entry数组。

Entry是什么呢?

Entry是一组组数据对, 而且继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

key 就是 ThreadLocal,肯定不为空,但也是弱引用的。

也就是说,当 key 为 null 时,说明 ThreadLocal 已经被回收了,对应的 Entry 就应该被清除了。

ThreadLocalMap.set()方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    //根据hashCode与长度计算索引位置
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         // 如果下标冲突, 索引+1继续查找
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        
        //找到直接返回值
        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            // key 为空, 说明 对应的 ThreadLocal 已经回收了.
            // 可以复用当前位置.
            // 有两种情况:1\. entry 存在, 在这个过时位置的后面. 所以需要置换到这个位置
            // 2.不存在, 直接放到这个位置
            replaceStaleEntry(key, value, i);
            // 因为是替换, 所以size 要么不变,要么减少。
            return;
        }
    }

    // 没找到已存在的, 也没找到可以替换的过时. 则直接新建
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 如果没有清除过时 entry, 并且超过阈值. 则进行先尝试缩小,不行则扩容
        rehash();
}

在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。

  • int i = key.threadLocalHashCode & (len-1)
  • int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

简而言之就是将threadLocalHashCode进行一个位运算(取模)得到索引i,threadLocalHashCode代码如下。

public class ThreadLocal<T> {  
    private final int threadLocalHashCode = nextHashCode();  

    private static AtomicInteger nextHashCode =  new AtomicInteger(); 
    private static final int HASH_INCREMENT = 0x61c88647;  

      
    private static int nextHashCode() {
        //自增
        return nextHashCode.getAndAdd(HASH_INCREMENT);  
    }
}

因为static的原因,在每次new ThreadLocal()时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为0x61c88647。

0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突。

有兴趣可以深入研究下去, 这里就不过多赘述了, 这里这样运算就是为了避免索引下标冲突。

总结一下:

  • 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。

  • 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

ThreadLocalMap.get()方法

public T get() {  
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取ThreadLocalMap
    ThreadLocalMap map = getMap(t);  
    if (map != null) {
        //通过ThreadLocal获取Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        //返回值
        if (e != null) {  
            @SuppressWarnings("unchecked")  
            T result = (T)e.value;  
            return result;  
        }  
    }
    //设置初始值--null
    return setInitialValue();  
}

private Entry getEntry(ThreadLocal<?> key) {
    //计算下标, 通过下标从Entry数组中直接取值
    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);  
}

理解了set方法,get方法也就清楚明了,直接通过计算出索引直接从数组对应位置读取即可。

ThreadLocalMap.remove()方法

public void remove() {  
    ThreadLocalMap m = getMap(Thread.currentThread());  
    if (m != null)  
        m.remove(this);  
}

ThreadLocal的垃圾回收机制

ThreadLocal对象的垃圾回收机制比较特殊,主要涉及到两个对象:ThreadLocal对象和ThreadLocalMap对象。

每个ThreadLocal对象都会在当前线程的ThreadLocalMap中创建一个Entry对象,这个Entry对象包含了ThreadLocal对象和其对应的值。当ThreadLocal对象没有被其他对象引用,并且当前线程结束时,这个ThreadLocal对象会被标记为可回收的,并且被添加到一个特殊的ReferenceQueue中。

当垃圾回收器扫描到ReferenceQueue中的ThreadLocal对象时,它会将ThreadLocal对象对应的Entry对象从ThreadLocalMap中删除,并且清除Entry对象中对ThreadLocal对象和值的引用,从而使得ThreadLocal对象和值都能够被回收。

需要注意的是,虽然ThreadLocal对象被回收了,但是它在ThreadLocalMap中对应的Entry对象并没有被立即清除,只有在下一次调用ThreadLocalMap的set()、get()或remove()方法时才会触发Entry对象的清除操作。这是因为ThreadLocalMap中的Entry对象使用了弱引用,只有在下一次调用ThreadLocalMap时才会被垃圾回收器扫描到并被清除。

因此,使用ThreadLocal对象时需要注意,在不再需要使用ThreadLocal对象时,应该及时调用remove()方法,以便及时清除ThreadLocalMap中对应的Entry对象,从而避免内存泄漏。

ThreadLocal的使用场景

参数透传

当我们在写API接口的时候,通常Controller层会接受来自前端的入参,当这个接口功能比较复杂的时候,可能我们调用的Service层内部还调用了很多其他的很多方法,通常情况下,我们会在每个调用的方法上加上需要传递的参数。

但是如果我们将参数存入ThreadLocal中,那么就不用显式的传递参数了,而是只需要ThreadLocal中获取即可。

这个场景其实使用的比较少,一方面显式传参比较容易理解,另一方面我们可以将多个参数封装为对象去传递。

全局存储用户信息(项目中用到)

在现在的系统设计中,前后端分离已基本成为常态,分离之后如何获取用户信息就成了一件麻烦事,通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。

在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的, 后续会出文章详解)。

当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类存入ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空(这一点很重要,否则会产生内存泄漏), 中间的过程无需再关注如何获取用户信息,只需要使用工具类的get方法即可。

解决线程安全问题

ThreadLocal的设计天然就做到了线程隔离。所以就不会出现线程安全问题。

以上就是Java多线程之ThreadLocal浅析的详细内容,更多关于Java多线程ThreadLocal的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java多线程之ThreadLocal浅析

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

下载Word文档

猜你喜欢

Java多线程之ThreadLocal浅析

这篇文章主要分析了Java多线程ThreadLocal,ThreadLocal叫做线程变量,用于在多线程环境下创建线程本地变量。想了解更多的可以参考本文
2023-05-17

Java多线程之ThreadLocal原理总结

这篇文章主要介绍了Java多线程ThreadLocal原理,同一个ThreadLocal所包含的对象,在不同的Thread中有不同的副本,文章中有详细的代码示例,需要的朋友参考一下
2023-05-15

Java多线程之ThreadLocal的原理是什么

今天小编给大家分享一下Java多线程之ThreadLocal的原理是什么的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。1、什
2023-07-06

怎么实现JAVA 多线程的浅析

这期内容当中小编将会给大家带来有关怎么实现JAVA 多线程的浅析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。JAVA 的多线程浅析一、JAVA 语言的来源、及特点在这个高速信息的时代,商家们纷纷把信息、
2023-06-03

深入浅析Java项目中的多线程

这期内容当中小编将会给大家带来有关深入浅析Java项目中的多线程,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。Java多线程实例 3种实现方法Java中的多线程有三种实现方式:1.继承Thread类,重写
2023-05-31

深入浅析Java中多线程优先级

这篇文章给大家介绍深入浅析Java中多线程优先级,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。Java 多线程优先级实例详解线程的优先级将该线程的重要性传递给调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器
2023-05-31

如何浅析Java多线程程序的设计机制

本篇文章为大家展示了如何浅析Java多线程程序的设计机制,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。浅析Java多线程程序设计机制多线程是Java语言的一大特性,多线程就是同时存在N个执行体,按几
2023-06-03

Java多线程之Interrupt中断线程的示例分析

小编给大家分享一下Java多线程之Interrupt中断线程的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!一、测试代码https://gitee.com/zture/spring-test/blob/master
2023-06-15

Java多线程编程中ThreadLocal类的用法及深入

ThreadLocal,直译为“线程本地”或“本地线程”,如果你真的这么认为,那就错了!其实,它就是一个容器,用于存放线程的局部变量,我认为应该叫做 ThreadLocalVariable(线程局部变量)才对,真不理解为什么当初 Sun 公
2022-06-04

Java多线程之死锁的示例分析

小编给大家分享一下Java多线程之死锁的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!什么是死锁?死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者
2023-05-30

Java多线程之Park和Unpark原理分析

这篇文章主要介绍了Java多线程之Park和Unpark原理分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。Java的优点是什么1. 简单,只需理解基本的概念,就可以编写适
2023-06-14

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录