面试高频 ThreadLocal类详解

简介: 面试高频 ThreadLocal类详解

前言

本文将从ThreadLocal的作用以及源码来进行对其的详细分析,包括内存泄漏以及其的本质原因等等

首先是ThreadLocal能干啥的问题,我将其总结为三点

1.传递数据:实现在公共组件中传递信息的需求

2.线程隔离:实现每个线程都是不相互影响的

3.线程并发:在多线程场景下完成需求

常用方法

常用的就是四个方法

1.构造方法

2.set方法

将变量绑定到线程上

3.get方法

获取绑定的线程

4.remove方法

移除绑定的变量(entry)

结构分析

我们这里直接考虑jdk8之后的结构而不考虑之前的结构了

这里是每一个thread线程持有一个threadlocalmap

是一个map结构,里面存放的是一个个entry

entry的key是threadlocal对象

entry的值是线程绑定的变量值

这样设计的优势:

1. 每个map存储的entry数量变少了 因为之前是由thread的数量决定的 现在的entry是由threadlocal决定

2.当thread销毁的时候threadlocalmap也会随之销毁,减少内存的使用

ThreadLocal和synchronized的区别与联系

我们知道解决多线程的并发问题,最常见的思想就是加锁

我们很轻易的就能想到Synchronized关键字

但是使用synchronized关键字加锁会导致程序的性能降低

主要的是两者的思想不同

synchronized主要思想使用时间换空间,侧重于多线程程序的同步性

threadlocal主要是用空间换时间

主要侧重于线程之间的隔离

一个简单的案例分析

假设我们这里需要做一个经典的转账接口,我们故意设置a扣费之后进行报错,使得b不能收到转账,这样a的转账就不翼而飞了,我们希望使用这两个操作来解决这个问题

思路1:

使用事务和加锁来解决问题

我们将这转账一系列操作使用事务来完成,但是我们就得注意这里服务分层了

业务层和数据层之间的连接必须要保持一致,这样就多了一个传参以及加锁的性能损耗

思路2:

使用threadlocal来操作

这样既不需要多付出性能的代价,也降低的层之间的耦合性,虽然增加了空间损耗,但是我们知道对于现在来说空间是比较不值钱的,相对来说效果更优.

源码分析

下面我们对几个常用方法进行源码的解析

set方法

先获取线程对应的threadlocalmap

获取到了就直接设置值

获取不到就创建一个新的map并设置初始值

get方法

首先获取当前线程,根据当前线程获取一个map

如果map不为空,就根据threadlocal找到对应的entry返回value,

如果map为空则通过initialValue函数获取初始值value

然后根据threadlocal的引用和value创建一个新的map

remove方法

如果发现对应的entry,流程是先获取到thread对应的threadlocalmap,存在就移除entry

不存在就啥也不干

ThreadLocalMap的分析

threadlocalMap是threadlocal的一个静态内部类

threadlocalmap是threadlocal独立实现的,没有实现任何的map接口等,以下是他的结构图

他的entry也是自己定义的,没有继承或者实现某些接口

下面我们来看看entry的内部定义源码

我们发现这里的entry是实现的弱引用,为什么要使用弱引用呢,我们在后面继续聊

内存泄露

首先我们谈谈内存泄露的定义

这里大致有两种定义

一种是内存溢出(用户分配的内存的空间不够用)的原因

可能可以使用调大设置内存空间来解决问题

另一种是系统的总内存不足以使用(满足不了程序的需求)

网上有想法说:内存泄露是因为使用若引用的缘故

其实不然,我们下面开始分别谈谈使用弱引用和强引用之间的区别,最后谈谈内存泄露的真实原因到底是什么.

强引用   被强引用引用的时候,不会被gc回收

弱引用  被弱引用引用的时候,被gc发现即回收

使用弱引用

这里如果我们不使用remove方法,只要线程还是存在的,这里指向entry的引用就会存在,entry仍然不会删除

虽然这里的threadlocal被置空了,但是也只是threadlocal被回收了,而entry不会被回收

强引用

仍然是始终有强引用链指向entry,不会被系统gc回收

本质原因

其本质原因主要和引用的强弱与否用处不大

本质上是thread和entry的生命周期是一样的,使用弱引用相对来说更加有保障一点

因为在调用get set方法等的时候,如果发现这里的entry为空就会自动给置空

这样可以一定程序上防止内存泄露的问题

解决哈希冲突

我们知道threadlocalmap是一个map本质上还是会出现hash冲突的情况

那么我们会以什么方式来解决hash冲突的呢?

我们从源码上来分析问题

这里的hash计算就是上述代码中i的计算

我们发现他还有一个函数和一个INITIAL_CAPICITY

下面我们再往里面走一步看看

这里使用了原子类的getAndAdd方法每次获取并加上一个十六进制的值

这个值主要和斐波那契数列那个黄金分割值有关,目的是让hash值均匀的分布在2的n次方的数组中,可以用来解决hash冲突问题

这里还与上了一个capacity-1其目的是让其最后生成的hash值不会溢出

因为下面使用的是线性探测的方式来解决hash冲突

相关文章
|
4月前
|
安全 Java 容器
【Java集合类面试二十七】、谈谈CopyOnWriteArrayList的原理
CopyOnWriteArrayList是一种线程安全的ArrayList,通过在写操作时复制新数组来保证线程安全,适用于读多写少的场景,但可能因内存占用和无法保证实时性而有性能问题。
|
4月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
4月前
|
Java
【Java集合类面试二十八】、说一说TreeSet和HashSet的区别
HashSet基于哈希表实现,无序且可以有一个null元素;TreeSet基于红黑树实现,支持排序,不允许null元素。
|
4月前
|
Java
【Java集合类面试二十三】、List和Set有什么区别?
List和Set的主要区别在于List是一个有序且允许元素重复的集合,而Set是一个无序且元素不重复的集合。
|
4月前
|
Java
【Java集合类面试二十六】、介绍一下ArrayList的数据结构?
ArrayList是基于可动态扩展的数组实现的,支持快速随机访问,但在插入和删除操作时可能需要数组复制而性能较差。
|
4月前
|
存储 Java 索引
【Java集合类面试二十四】、ArrayList和LinkedList有什么区别?
ArrayList基于动态数组实现,支持快速随机访问;LinkedList基于双向链表实现,插入和删除操作更高效,但占用更多内存。
|
22天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
76 4
|
3月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
2月前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
26 1
|
3月前
|
安全 Java 数据库连接
反问面试官3个ThreadLocal的问题
接下来,我想先说说ThreadLocal的用法和使用场景,然后反问面试官3个关于ThreadLocal的话题。
反问面试官3个ThreadLocal的问题