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

}

输出结果:

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

}

输出结果:

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

}

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

相关文章
|
4天前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
48 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
11天前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
Java 并发编程——volatile 关键字解析
|
9天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
11天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
11天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
9天前
|
Java 数据库连接 Spring
反射-----浅解析(Java)
在java中,我们可以通过反射机制,知道任何一个类的成员变量(成员属性)和成员方法,也可以堆任何一个对象,调用这个对象的任何属性和方法,更进一步我们还可以修改部分信息和。
|
11天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
35 3
|
11天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
93 2
|
5月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
142 1
|
8月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
245 2

推荐镜像

更多