Java多线程之深入解析ThreadLocal和ThreadLocalMap

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

Java多线程之深入解析ThreadLocal和ThreadLocalMap

ThreadLocal概述
ThreadLocal是线程变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

它具有3个特性:

线程并发:在多线程并发场景下使用。
传递数据:可以通过ThreadLocal在同一线程,不同组件中传递公共变量。
线程隔离:每个线程变量都是独立的,不会相互影响。
在不使用ThreadLocal的情况下,变量不隔离,得到的结果具有随机性。

public class Demo {

private String variable;

public String getVariable() {
    return variable;
}

public void setVariable(String variable) {
    this.variable = variable;
}

public static void main(String[] args) {
    Demo demo = new Demo();
    for (int i = 0; i < 5; i++) {
        new Thread(()->{
            demo.setVariable(Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
        }).start();
    }
}
AI 代码解读

}

输出结果:

 View Code
在不使用ThreadLocal的情况下,变量隔离,每个线程有自己专属的本地变量variable,线程绑定了自己的variable,只对自己绑定的变量进行读写操作。

public class Demo {

private ThreadLocal<String> variable = new ThreadLocal<>();

public String getVariable() {
    return variable.get();
}

public void setVariable(String variable) {
    this.variable.set(variable);
}

public static void main(String[] args) {
    Demo demo = new Demo();
    for (int i = 0; i < 5; i++) {
        new Thread(()->{
            demo.setVariable(Thread.currentThread().getName());
            System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
        }).start();
    }
}
AI 代码解读

}

输出结果:

 View Code
synchronized和ThreadLocal的比较
上述需求,通过synchronized加锁同样也能实现。但是加锁对性能和并发性有一定的影响,线程访问变量只能排队等候依次操作。TreadLocal不加锁,多个线程可以并发对变量进行操作。

public class Demo {

private String variable;
public String getVariable() {
    return variable;
}

public void setVariable(String variable) {
    this.variable = variable;
}

public static void main(String[] args) {
    Demo demo = new Demo1();
    for (int i = 0; i < 5; i++) {
        new Thread(()->{
            synchronized (Demo.class){
                demo.setVariable(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+" "+demo.getVariable());
            }
        }).start();
    }
}
AI 代码解读

}

ThreadLocal和synchronized都是用于处理多线程并发访问资源的问题。ThreadLocal是以空间换时间的思路,每个线程都拥有一份变量的拷贝,从而实现变量隔离,互相不干扰。关注的重点是线程之间数据的相互隔离关系。synchronized是以时间换空间的思路,只提供一个变量,线程只能通过排队访问。关注的是线程之间访问资源的同步性。ThreadLocal可以带来更好的并发性,在多线程、高并发的环境中更为合适一些。

ThreadLocal使用场景
转账事务的例子
JDBC对于事务原子性的控制可以通过setAutoCommit(false)设置为事务手动提交,成功后commit,失败后rollback。在多线程的场景下,在service层开启事务时用的connection和在dao层访问数据库的connection应该要保持一致,所以并发时,线程只能隔离操作自已的connection。

解决方案1:service层的connection对象作为参数传递给dao层使用,事务操作放在同步代码块中。

存在问题:传参提高了代码的耦合程度,加锁降低了程序的性能。

解决方案2:当需要获取connection对象的时候,通过ThreadLocal对象的get方法直接获取当前线程绑定的连接对象使用,如果连接对象是空的,则去连接池获取连接,并通过ThreadLocal对象的set方法绑定到当前线程。使用完之后调用ThreadLocal对象的remove方法解绑连接对象。

ThreadLocal的优势:

可以方便地传递数据:保存每个线程绑定的数据,需要的时候可以直接获取,避免了传参带来的耦合。
可以保持线程间隔离:数据的隔离在并发的情况下也能保持一致性,避免了同步的性能损失。
ThreadLocal的原理
每个ThreadLocal维护一个ThreadLocalMap,Map的Key是ThreadLocal实例本身,value是要存储的值。

每个线程内部都有一个ThreadLocalMap,Map里面存放的是ThreadLocal对象和线程的变量副本。Thread内部的Map通过ThreadLocal对象来维护,向map获取和设置变量副本的值。不同的线程,每次获取变量值时,只能获取自己对象的副本的值。实现了线程之间的数据隔离。

JDK1.8的设计相比于之前的设计(通过ThreadMap维护了多个线程和线程变量的对应关系,key是Thread对象,value是线程变量)的好处在于,每个Map存储的Entry数量变少了,线程越多键值对越多。现在的键值对的数量是由ThreadLocal的数量决定的,一般情况下ThreadLocal的数量少于线程的数量,而且并不是每个线程都需要创建ThreadLocal变量。当Thread销毁时,ThreadLocal也会随之销毁,减少了内存的使用,之前的方案中线程销毁后,ThreadLocalMap仍然存在。

ThreadLocal源码解析
set方法
首先获取线程,然后获取线程的Map。如果Map不为空则将当前ThreadLocal的引用作为key设置到Map中。如果Map为空,则创建一个Map并设置初始值。

get方法
首先获取当前线程,然后获取Map。如果Map不为空,则Map根据ThreadLocal的引用来获取Entry,如果Entry不为空,则获取到value值,返回。如果Map为空或者Entry为空,则初始化并获取初始值value,然后用ThreadLocal引用和value作为key和value创建一个新的Map。

remove方法
删除当前线程中保存的ThreadLocal对应的实体entry。

initialValue方法
该方法的第一次调用发生在当线程通过get方法访问线程的ThreadLocal值时。除非线程先调用了set方法,在这种情况下,initialValue才不会被这个线程调用。每个线程最多调用依次这个方法。

该方法只返回一个null,如果想要线程变量有初始值需要通过子类继承ThreadLocal的方式去重写此方法,通常可以通过匿名内部类的方式实现。这个方法是protected修饰的,是为了让子类覆盖而设计的。

ThreadLocalMap源码分析
ThreadLocalMap是ThreadLocal的静态内部类,没有实现Map接口,独立实现了Map的功能,内部的Entry也是独立实现的。

与HashMap类似,初始容量默认是16,初始容量必须是2的整数幂。通过Entry类的数据table存放数据。size是存放的数量,threshold是扩容阈值。

Entry继承自WeakReference,key是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

弱引用和内存泄漏
内存溢出:没有足够的内存供申请者提供

内存泄漏:程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等验证后沟。内存泄漏的堆积会导致内存溢出。

弱引用:垃圾回收器一旦发现了弱引用的对象,不管内存是否足够,都会回收它的内存。

内存泄漏的根源是ThreadLocalMap和Thread的生命周期是一样长的。

如果在ThreadLocalMap的key使用强引用还是无法完全避免内存泄漏,ThreadLocal使用完后,ThreadLocal Reference被回收,但是Map的Entry强引用了ThreadLocal,ThreadLocal就无法被回收,因为强引用链的存在,Entry无法被回收,最后会内存泄漏。

在实际情况中,ThreadLocalMap中使用的key为ThreadLocal的弱引用,value是强引用。如果ThreadLocal没有被外部强引用的话,在垃圾回收的时候,key会被清理,value不会。这样ThreadLocalMap就出现了为null的Entry。如果不做任何措施,value永远不会被GC回收,就会产生内存泄漏。

ThreadLocalMap中考虑到这个情况,在set、get、remove操作后,会清理掉key为null的记录(将value也置为null)。使用完ThreadLocal后最后手动调用remove方法(删除Entry)。

也就是说,使用完ThreadLocal后,线程仍然运行,如果忘记调用remove方法,弱引用比强引用可以多一层保障,弱引用的ThreadLocal会被回收,对应的value会在下一次ThreadLocalMap调用get、set、remove方法的时候被清除,从而避免了内存泄漏。

Hash冲突的解决
ThreadLocalMap的构造方法

构造函数创建一个长队为16的Entry数组,然后计算firstKey的索引,存储到table中,设置size和threshold。

firstKey.threadLocalHashCode & (INITIAL_CAPACITY-1)用来计算索引,nextHashCode是Atomicinteger类型的,Atomicinteger类是提供原子操作的Integer类,通过线程安全的方式来加减,适合高并发使用。

每次在当前值上加上一个HASH_INCREMENT值,这个值和斐波拉契数列有关,主要目的是为了让哈希码可以均匀的分布在2的n次方的数组里,从而尽量的避免冲突。

当size为2的幂次的时候,hashCode & (size - 1)相当于取模运算hashCode % size,位运算比取模更高效一些。为了使用这种取模运算, 所有size必须是2的幂次。这样一来,在保证索引不越界的情况下,减少冲突的次数。

ThreadLocalMap的set方法

ThreadLocalMao使用了线性探测法来解决冲突。线性探测法探测下一个地址,找到空的地址则插入,若整个空间都没有空余地址,则产生溢出。例如:长度为8的数组中,当前key的hash值是6,6的位置已经被占用了,则hash值加一,寻找7的位置,7的位置也被占用了,回到0的位置。直到可以插入为止,可以将这个数组看成一个环形数组。

原文地址https://www.cnblogs.com/xdcat/p/13051561.html

目录
打赏
0
0
0
0
47
分享
相关文章
|
1月前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
171 60
【Java并发】【线程池】带你从0-1入门线程池
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
72 23
|
27天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
98 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
144 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
64 13
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
2月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
141 17
|
3月前
|
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
Java 多线程 面试题
Java 多线程 相关基础面试题
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。

推荐镜像

更多