理解Java中的ThreadLocal
引子
现在有这样一个需求,在多线程并发的场景下,要求每个线程中的变量都是相互独立的
举个例子:线程 1 为变量 A 设置一个值 1,那么他获取到值的只能是线程 1 设置进去的值 ;线程 2 为变量 A 设置一个值 2,那么他获取到的值的只能是线程 2 设置进去的值
下面是一个 Demo
1 | public class Demo { |
多测试几次,有时候就会出现的运行结果
线程 2—->取出线程 3 设置的值
线程 4—->取出线程 4 设置的值
线程 3—->取出线程 3 设置的值
线程 1—->取出线程 1 设置的值
线程 0—->取出线程 3 设置的值
ThreadLocal 简介
ThreadLocal 是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法
ThreadLocal 是 java.lang
包下的一个类,它能提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问 ThreadLocal 实例的时候(通过 get 或 set 方法)都有自己独立初始化的变量副本,即这种变量能在多线程环境下访问(即通过 get 和 set 方法访问)时能保证各个线程的变量相对独立于其他线程内的变量(变量在线程的生命周期内起作用)。
如果创建一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,线程之间是相互隔离的,各个线程操作的都是自己本地内存中的变量,从而规避了线程安全问题。
ThreadLocal 实例通常来说都是 private static
类型的,用于关联线程和线程上下文。
案列改造
利用 ThreadLocal 的 set 方法和 get 方法来对上述案例进行改造
1 | /** |
此时再进行多次测试,线程就只能获取到自己设置进去的值了
与 synchronized 区别
通过 synchronized 关键字也可以解决问题,将当前类作为锁对象即可
1 | Thread thread = new Thread(() -> { |
ThreadLocal 与 synchronized 的区别
虽然 ThreadLocal 模式与 synchronized 关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
synchronized | ThreadLcocal | |
---|---|---|
原理 | 时间换空间 只提供了一份变量让不同的线程排队访问 | 空间换时间 为每一个线程都提供了一份变量的副本从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
ThreadLocal 设计方案
在 JDK8 中 ThreadLocal 的设计方案是:
每个 Thread 维护一个 ThreadLocalMap,这个 Map 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 Object
具体的过程
- 每个 Thread 线程内部都有一个 Map(ThreadLocalMap,由 Thread 维护)
- Map 里面存储 ThreadLocal 对象(作为 key)和线程的变量副本(value),由 ThreadLocal 负责向 map 获取和设置线程的变量值
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离互不干扰
好处
- ThreadLocal 负责设置值,可以使 Map 中储存的 Entry 数量变少(如果由 Thread 设置值,多少个线程就有多少个 key-value)
- Thread 使用完毕销毁时,ThreadLocalMap 也会跟着销毁,可以减少内存的开销(如果是 ThreadLocalMap 维护,线程销毁 Map 还在)
核心方法源码分析
除了构造方法之外, ThreadLocal 对外暴露的方法有以下 4 个
方法声明 | 描述 |
---|---|
protected T initialValue() | 返回当前线程本地变量的初始值 |
public void set( T value) | 设置当前线程绑定的本地变量 |
public T get() | 获取当前线程绑定的本地变量 |
public void remove() | 移除当前线程绑定的本地变量 |
set 方法
设置当前线程对应的 ThreadLocal 的值
1 | public void set(T value) { |
先看一下 getMap()
方法
1 | /** |
看一下返回的 threadLocals
,可以发现位于 Thread
类的内部,并且默认值为 null
1 | /*与此线程有关的 ThreadLocal 值。该map由 ThreadLocal 类维护*/ |
再看一下createMap()
方法
1 | void createMap(Thread t, T firstValue) { |
get 方法
先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值
1 | public T get() { |
进入 setInitialValue()
:初始化值并返回
1 | private T setInitialValue() { |
remove 方法
获取当前线程,并根据当前线程获取一个 Map,如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 entry
1 | /** |
initialValue 方法
1 | /** |
内存泄漏
每个线程内部有一个名为 threadLocals 的成员变量,该变量的类型为 ThreadLocal.ThreadLocalMap
类型(在上面 set 方法里面提到过,位于 Thread
类的内部,可以理解为类似于一个 HashMap),其中的 key 为当前定义的 ThreadLocal 变量的 this 引用,value 为我们使用 set 方法设置的值。每个线程的本地变量存放在自己的本地内存变量 threadLocals 中
如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其 remove 掉。
比如说创建的线程池有核心线程是一直存在的,那么它维护的 ThreadLocalMap 也是一直存在的,所以可能会导致内存溢出。