JUC笔记(二)ThreadLocal
ThreadLocal
ThreadLocal是什么
感谢:
(1 封私信) Java中ThreadLocal的实际用途是啥? - 知乎 (zhihu.com)
ThreadLocal使用与原理_敖 丙的博客-CSDN博客_threadlocal使用
ThreadLocal的介绍+经典应用场景 - 掘金 (juejin.cn)十分推荐阅读,思路清晰。
ThreadLocal
的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
在JDK 1.2的版本中就提供
java.lang.ThreadLocal
,ThreadLocal
为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
ThreadLocal
并不是一个Thread
,而是Thread
的局部变量,也许把它命名为ThreadLocalVariable
更容易让人理解一些。在JDK5.0中,
ThreadLocal
已经支持泛型,该类的类名已经变为ThreadLocal<T>
。API方法也相应进行了调整,新版本的API方法分别是void set(T value)
、T get()
以及T initialValue()
。
在处理多线程并发安全的方法中,最常用的方法,就是使用锁,通过锁来控制多个不同线程对临界区的访问。但是,无论是什么样的锁,乐观锁或者悲观锁,都会在并发冲突的时候对性能产生一定的影响。那有没有一种方法,可以彻底避免竞争呢?
答案是肯定的,这就是ThreadLocal。
从字面意思上看,ThreadLocal
可以解释成线程的局部变量,也就是说一个ThreadLocal
的变量只有当前自身线程可以访问,别的线程都访问不了,那么自然就避免了线程竞争。
ThreadLocal
类用来提供线程内部的局部变量。这种局部变量在多线程的环境下访问(通过get
和set
方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal
实例通常来说都是private static
类型的,用于关联线程和线程上下文。
因此,ThreadLocal
提供了一种与众不同的线程安全方式,它不是在发生线程冲突时想办法解决冲突,而是彻底的避免了冲突的发生。

总结
ThreadLocal
又叫做线程局部变量,全称thread local variable
,它的使用场合主要是为了解决多线程中因为数据并发产生不一致的问题。ThreadLocal
为每一个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果无非是耗费了内存,也大大减少了线程同步所带来的性能消耗,也减少了线程并发控制的复杂度。
总的来说:ThreadLocal
适用于每一个线程需要自己独立实例,而且实例的话需要在多个方法里被使用到,也就是变量在线程之间是隔离的,但是在方法或者是类里面是共享的场景。
ThreadLocal基本使用
public class Main {
//创建一个`ThreadLocal`对象:
//private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
//因为ThreadLocal里面设置的值,只有当前线程可见,也就以为着你不可能通过其他线程为它初始化值
//为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值:
//ThreadLocal的初始值设置为6,这对全体线程都是可见的
//private ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 6);
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
IntStream.range(0, 10).forEach(i -> new Thread(() -> {
threadLocal.set(Thread.currentThread().getName() + ":" + i);
System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + threadLocal.get());
}).start());
}
//public int sendAndGet() {
//threadLocal.set(8);
//return threadLocal.get();
//}
}

每一个线程都有自己的local 值,这就是TheadLocal
的基本使用 。
package org.uin.ThreadLocal;
/**
* @author wanglufei
* @description: TODO
* @date 2022/3/19/2:41 PM
*/
public class ThreadLocalTest {
private static ThreadLocal<String> local = new ThreadLocal<String>();
static void print(String str) {
//打印当前线程中本地内存中变量的值
System.out.println(str + " :" + local.get());
//清除内存中的本地变量
local.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
ThreadLocalTest.local.set("BearBrick0");
print("A");
//打印本地变量
System.out.println("清除后:" + local.get());
}
},"A").start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
ThreadLocalTest.local.set("BearBrick0");
print("B");
System.out.println("清除后 " + local.get());
}
},"B").start();
}
}
运行后可以看到BearBrick0的值为null,BearBrick0的值也为null,表明了两个线程都分别获取了自己线程存放的变量,他们之间获取到的变量不会错乱。
ThreadLocal的实现原理
get()
ThreadLocal
变量只在单个线程内可见,那它是如何做到的呢?我们先从最基本的get()
方法说起:
public T get() {
//获得当前线程
Thread t = Thread.currentThread();
//每个线程 都有一个自己的ThreadLocalMap,
//ThreadLocalMap里就保存着所有的ThreadLocal变量
ThreadLocalMap map = getMap(t);
if (map != null) {
//ThreadLocalMap的key就是当前ThreadLocal对象实例,
//多个ThreadLocal变量都是放在这个map中的
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//从map里取出来的值就是我们需要的这个ThreadLocal变量
T result = (T)e.value;
return result;
}
}
// 如果map没有初始化,那么在这里初始化一下
return setInitialValue();
}
可以看到,所谓的ThreadLocal
变量就是保存在每个线程的map
中的。这个map就是Thread
对象中的threadLocals
字段。如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap
是一个比较特殊的Map
,它的每个Entry
的key
都是一个弱引用:


static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//key就是一个弱引用
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通过上面的分析,相信你对该方法已经有所理解了,首先获取当前线程,然后通过key threadlocal
获取 设置的value
。
这样设计的好处是,如果这个变量不再被其他对象使用时,GC可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value,依然是强引用,如何回收,见下文分解)。
set()
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//首先获取当前线程对象
Thread t = Thread.currentThread();
//获取线程中变量 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果不为空,
if (map != null)
map.set(this, value);
else
//如果为空,初始化该线程对象的map变量,其中key 为当前的threadlocal 变量
createMap(t, value);
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
//初始化线程内部变量 threadLocals ,key 为当前 threadlocal
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
汇总下,ThreadLocalMap
为 ThreadLocal
的一个静态内部类,里面定义了Entry
来保存数据。而且是继承的弱引用。在Entry
内部使用ThreadLocal
作为key
,使用我们设置的value
作为value
。
对于每个线程内部有个ThreadLocal.ThreadLocalMap
变量,存取值的时候,也是从这个容器中来获取。
在使用完ThreadLocal
,推荐要调用一下remove()
方法,这样会防止内存溢出这种情况的发生,因为ThreadLocal
为弱引用。如果ThreadLocal
在没有被外部强引用的情况下,在垃圾回收的时候是会被清理掉的,如果是强引用那就不会被清理。
总结
每一个 Thread
对象均含有一个 ThreadLocalMap
类型的成员变量 threadLocals
,它存储本线程中所有ThreadLocal
对象及其对应的值。
ThreadLocalMap
由一个个 Entry
对象构成Entry
继承自 WeakReference<ThreadLocal<?>>
,一个 Entry
由 ThreadLocal
对象和 Object
构成。
由此可见, Entry
的key
是ThreadLocal
对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。
当执行set
方法时,ThreadLocal
首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap
对象。再以当前ThreadLocal
对象为key
,将值存储进ThreadLocalMap
对象中。
get
方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap
对象。再以当前ThreadLocal
对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap
容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
有什么应用场景
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized
来保证同一时刻只有一个线程对共享变量进行操作。
ThreaLocal
作用在每个线程内都都需要独立的保存信息,这样就方便同一个线程的其他方法获取到该信息的场景,由于每一个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息之后,后续方法可以通过ThreadLocal可以直接获取到,避免了传参,这个类似于全局变量的概念。比如像用户登录令牌解密后的信息传递、用户权限信息、从用户系统中获取到的用户名
//用户微服务配置token解密信息传递例子
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
LoginUser loginUser = new LoginUser();
loginUser.setId(id);
loginUser.setName(name);
loginUser.setMail(mail);
loginUser.setHeadImg(headImg);
threadLocal.set(loginUser);
//后续想直接获取到直接threadLocal.getxxx就可以了
如何使用ThreadLocal来解决线程安全的问题
在我们平常的SpringWeb项目中,我们通常会把业务分成Controller
、Service
、Dao
等等,也知道注解@Autowired
默认使用单例模式。
那有没有想过,当不同的请求线程进来后,因为Dao
层使用的是单例,那么负责连接数据库的Connection
也只有一个了,这时候如果请求的线程都去连接数据库的话,就会造成这个线程不安全的问题,Spring
是怎样来解决的呢?
在Dao
层里装配的Connection
线程肯定是安全的,解决方案就是使用ThreadLocal
方法。
当每一个请求线程使用Connection
的时候,都会从ThreadLocal
获取一次,如果值为null
,那就说明没有对数据库进行连接,连接后就会存入到 ThreadLocal
里,这样一来,每一个线程都保存有一份属于自己的Connection
。
每一线程维护自己的数据,达到线程的隔离效果。
最常见的ThreadLocal
使用场景:
-
在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
-
线程间数据隔离
-
进行事务操作,用于存储线程事务信息。
-
数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的
connection
来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的
ThreadLocal
来实现这种隔离
ThreadLocal作用、场景、原理 - 简书 (jianshu.com)
面试官:不懂ThreadLocal,还谈什么并发编程? - 简书 (jianshu.com)
ThreadLocal内存泄露原因,如何避免
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

注释说,Note that null keys (i.e. entry.get()* == null)
如果 key threadlocal
为 null
了,这个 entry
就可以清除了。
ThreadLocal
是一个弱引用,当为null
时,会被当成垃圾回收 。
虽然ThreadLocalMap
中的key
是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry
中的value
依然是强引用。
-
在
Thread
类的源码中有一个threadLocals
,就是ThreadLocalMap
-
ThreadLocalMap
的Entry
中的key
是ThreadLocal
,值是我们自己设定的 -
ThreadLocal
是一个弱引用,当为null
时,会被当成垃圾被JVM回收 -
敲重点,如果
ThreadLocal
是null
了,也就是要被垃圾回收器回收了,但此时ThreadLocalMap
生命周期和Thread
是一样,它不会回收,这时候就出现了一个现象,ThreadLocalMap
的key
没了,但是value
还在,这就造成了内存泄漏。
可以看到,只有当Thread
被回收时,这个value
才有被回收的机会,否则,只要线程不退出,value
总是会存在一个强引用。但是,要求每个Thread
都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value
对象出现泄漏的可能。
ThreadLocal
的实现原理,每一个Thread
维护一个ThreadLocalMap
,key
为使用弱引用的ThreadLocal
实例,value
为线程变量的副本。

ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,如果一个ThreadLocal
不存在外部强引用时,Key(ThreadLocal)
势必会被GC回收,这样就会导致ThreadLocalMap
中key
为null
, 而value
还存在着强引用,只有Thread
线程退出以后,value
的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key
为null
的Entry
的value
就会一直存在一条强引用链(红色链条)
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal
正确的使用方法:
- 每次使用完
ThreadLocal
都调用它的remove()
方法清除数据 - 将
ThreadLocal
变量定义成private static
,这样就一直存在ThreadLocal
的强引用,也就能保证任何时候都能通过ThreadLocal
的弱引用访问到Entry的value值,进而清除掉 。
作者:BearBrick0
出处:https://www.cnblogs.com/bearbrick0/p/16026983.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」