JUC(八)ThreadLocal
ThreadLocal
简介
ThreadLocal提供局部线程变量,这个变量与普通的变量不同,每个线程在访问ThreadLocal实例的时候,(通过get或者set方法)都有自己的、独立初始化变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(用户ID或者事务ID)与线程关联起来。
每个线程都有自己的、独立初始化变量副本,意味着避免了线程安全问题
使用
- ThreadLocal
.withInitial():创建ThreadLocal并设置初始值 - initialValue():返回局部线程变量的当前线程的初始化值
- get():返回局部线程变量在当前线程副本的值
- set():设置局部线程变量在当前线程副本的值
- remove():删除线程的局部线程变量副本
案例
class House { int saleCount; ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0); public synchronized void saleHouse() { this.saleCount++; } public void saleVolumeByThreadLocal() { saleVolume.set(saleVolume.get() + 1); } } public class SaleHouse { public static void main(String[] args) { House house = new House(); for (int i = 0; i < 5; i++) { new Thread(() -> { int size = new Random().nextInt(5) + 1; try { for (int i1 = 0; i1 < size; i1++) { house.saleHouse(); house.saleVolumeByThreadLocal(); } System.out.println(Thread.currentThread().getName() + " sales " + house.saleVolume.get()); } finally { house.saleVolume.remove(); } }, String.valueOf(i)).start(); } try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Total: " + house.saleCount); } }
现在需要记录每个销售员的业绩,可以使用ThreaLocal设置每个线程变量进行存储
class House { int saleCount; ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0); public synchronized void saleHouse() { this.saleCount++; } public void saleVolumeByThreadLocal() { saleVolume.set(saleVolume.get() + 1); } } public class SaleHouse { public static void main(String[] args) { House house = new House(); for (int i = 0; i < 5; i++) { new Thread(() -> { int size = new Random().nextInt(5) + 1; for (int i1 = 0; i1 < size; i1++) { house.saleHouse(); house.saleVolumeByThreadLocal(); } System.out.println(Thread.currentThread().getName() + " sales " + house.saleVolume.get()); }, String.valueOf(i)).start(); } try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Total: " + house.saleCount); } }
- 由于是每个线程独有的一个副本变量,因此是绝对线程安全的,不需要将方法变为同步方法
清除局部变量
必须回收自定义的ThreadLocal变量,尤其是在线程池的场景下,线程经常会复用,如果不清理则会造成后续逻辑业务和内存泄漏问题,所以要在代码中使用try-catch进行回收。
ThreadLocal源码解读
Thread ThreadGroup ThreadLocal三者的区别?
- Thread中含有ThreadLocalGroup的属性,而ThreadLocalGroup是ThreadLocal的静态内部类
- ThreadLocalGroup中有一个继承了弱引用WeakReference的Entry内部类
- Entry维护了一个ky键值对,键为当前线程的ThreadLocal,值为任意对象
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... }

原话是:JVM内部维护了一个线程版的Map<ThreadLocal, value>(通过ThreadLocal对象的set方法,把ThreadLocal对象自己当作key放入到ThreadMap中),每个线程要用到的时候,用当前线程去自己的map里面以相应的ThreadLocal取,通过这样每个线程都拥有了独立的变量。
private T get(Thread t) { ThreadLocalMap map = getMap(t); // 首先获取当前线程的ThreadLocalMap if (map != null) { if (map == ThreadLocalMap.NOT_SUPPORTED) { return initialValue(); } else { ThreadLocalMap.Entry e = map.getEntry(this); // 再获取this(ThreadLocal)的值 if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result; } } } return setInitialValue(t); }
ThreadLocal实现数据隔离的原理

-
ThreaLocal只是一个外壳,真正起存储作用的是ThreadLocal的ThreadLocalMap这个内部类,ThreadLocal本身只是作为一个key好从ThreadLocalMap获取value
-
每个线程都维护了一个ThreadLocalMap引用,ThreadLocalMap使用Entry进行的存储,因此每个线程针对相同的ThreadLocal取得的值是不同的,因为他们的ThreadLocalMap是每个线程都有一个
-
ThreadLocal调用set方法时,就是往ThreadLocalMap设置值,key为ThreadLocal,值为传递来的对象
正因为上述原理,ThreadLocal才能实现每个线程拥有一份副本,实现数据隔离
为什么ThreadLocal 的 ThreadLocalMap要使用弱引用
内存泄漏
:不会被使用的对象和变量的内存不能被回收,就是内存泄漏
内存溢出
:内存泄漏达到一定程度导致无法分配内存,即OOM,就会导致内存溢出

回答这个问题,首先要看一下Java的四种引用:
对于普通的对象,如果没有其他的引用关系,只要超过了引用的作用域范围或者显式地赋值为null,就会被垃圾回收器回收
强引用 Reference
- 强引用是常见的普通对象引用,只要还有强引用指向一个对象,就表明对象还存活,垃圾回收器就不会碰这种对象
- 只要把一个对象赋值给一个引用变量,这个引用变量就是强引用
- 当一个对象被强引用变量引用,它就处于可达状态,垃圾回收器就不可能回收它
- 即使该对象以后永远也用不到,JVM也不会回收,因此强引用是造成JAVA泄漏的主要原因之一
class MyObject { @Override protected void finalize() throws Throwable { System.out.println("--- invoke finalize method!"); } } public class ReferenceDemo { public static void main(String[] args) { var object = new MyObject(); System.out.println("gc before:" + object); object = null; System.gc(); System.out.println("gc after:" + object); } }
gc before:com.hikaru.juc.threadLocal.MyObject@7cd84586 gc after:null --- invoke finalize method!
finalize是在对象被不可撤销的清理之前执行的撤销操作
System.gc() 人工gc
SoftReference 软引用
- 软引用是相对强引用弱化了一些的引用,可以让对象豁免一些垃圾收集
- 当内存充足的时候它不会被回收
- 当内存不足的时候就会被回收
- 所以经常使用在对内存敏感的程序中,如在高速缓存中
weakReference 弱引用
- 比软引用弱化的引用,只要gc就会被回收
虚引用

下面回到最初的问题,为什么ThreadLocal的ThreadLocalMap的Entry要使用弱引用:
ThreadLocal<String> t1 = new ThreadLocal<>(); t1.set("hello"); t1.get();

在创建ThreadLocal的时候,会有强引用变量指向ThreadLocal对象,并且ThreadLocalMap的Entity的key会以弱引用的方式同时指向该对象,当强引用的作用域过期则只有弱引用指向ThreaLocal,而如果不是使用弱引用则ThreaLocal对象就一直不能被垃圾回收从而导致内存泄漏问题。
ThreadLocal之清除脏Entity
问题分析
:当我们为ThreadLocal赋值时,实际上就是为当前线程的ThreadLocalMap中添加Entry键值对,而Entry中的key是弱引用,当ThreadLocal外部强引用被赋值为null,那么系统GC的时候,根据可达性分析,没有任何链路能够到达这个ThreadLocal实例,如此一来,ThreadLocalMap就出现了key为null的Entity。
接下来如果线程迟迟不能结束(线程池复用线程),这些key的对象就会由于ThreadLocalMap的强引用链导致永远无法回收,造成内存泄漏。因此,弱引用不能保证内存泄漏问题。
remove方法会寻找脏Entity,即key==null的Entity进行删除。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步