▎结构理解
- 每个Thread线程内部都只有一个ThreadLocalMap。
- Map里面存储线程本地对象ThreadLocal(key)和线程的变量副本(value)。
- Thread内部的Map是由ThreadLocal维护,ThreadLocal负责向map获取和设置线程的变量值
- 一个Thread可以有多个ThreadLocal
▎ThreadLocal场景
- 每个线程需要有自己单独的实例 (也可线程内部构建单独的实例,但ThreadLocal更方便)
- 实例不被多线程共享,但需在多个方法中共享(可通过方法间形参传递实现,但ThreadLocal降低耦合)
ThreadLocal 提供了线程本地的实例。每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被
private static
修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
① 单独实例
private static ThreadLocal<String> local = new ThreadLocal<>();
public static void main(String args[]) {
local.set("这是在主线程中");
println("线程名字:"+ Thread.currentThread().getName() + "---" + local.get());
// 线程a
new Thread(()->{
local.set("这是在线程a中");
println("线程名字:" + Thread.currentThread().getName() + "---" + local.get());
}, "线程a").start();
// 线程b
new Thread(()->{
local.set("这是在线程b中");
println("线程名字:" + Thread.currentThread().getName() + "---" + local.get());
}, "线程b").start();
}
输出结果:
线程名字:main---这是在主线程中
线程名字:线程a---这是在线程a中
线程名字:线程b---这是在线程b中
线程内部的ThreadLocalMap,存储了ThreadLocal为Key变量副本为Vaule的键值对。(隔离了)
② 方法参数传递
// 如下示例:执行一个大方法 SaveOrder() 中有N个小方法
public void SaveOrder(User user) {
checkPermission(); // 检查正则验证
doWork(); // 执行订单保存等
saveStatus(); // 保存订单状态
sendResponse(); // 响应保存结果
}
问题:如何在一个线程内传递状态?(假设传递就是user实例)
// 解决:直接每个方法都传递user ??
public void SaveOrder(User user) {
checkPermission(user);
doWork(user); // doSomething...
saveStatus(user);
sendResponse(user);
}
// 问题:往往一个方法又会调用其他很多方法,导致User传递到所有地方:
public void doWork(User user) {
queryStatus(user);
checkStatus();
setNewStatus(user);
}
解决:ThreadLocal 可以在一个线程中传递同一个对象
在一个线程中,横跨若干方法调用,需要传递的对象,通常称之为上下文(Context)它是一种状态,可以是用户身份、任务信息等。
每个方法都加入形参传递非常麻烦,且如果调用链有无法修改源码的第三方库,
User
对象就传不进去
它的典型使用方式如下:
// 通常以静态static修饰,涉及到延长生命周期,从而减少key值为null的频率产生
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
public void SaveOrder(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}
// 通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:
void step1() {
User u = threadLocalUser.get();
printUser();
}
void step2() {
User u = threadLocalUser.get();
checkUser(u.id);
}
▎ThreadLocal 使用注意
- ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
- ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递)
- 使用ThreadLocal要用try ... finally结构,并在finally中清除。
因为当前线程执行完相关代码后,可能会被重新放入线程池中,如果
ThreadLocal
没有被清除,该线程执行其他代码时,会把上一次的状态带进去
▎源码解析
问题: ThreadLocal是怎么把变量复制到Thread的ThreadLocalMap中的?
当我们初始化一个线程时,其内部就去创建了一个ThreadLocalMap的Map容器待用
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap 被创建加载时,其静态内部类Entry也随之加载,完成初始化动作:
public class ThreadLocal<T> {
...
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
问题:那么它又是如何和ThreadLocal缠上关系,ThreadLocal又是如何管理键值对的关系
⭐️ set方法
public class ThreadLocal<T> {
public void set(T value) {
// 1.获取当前线程Thread
Thread t = Thread.currentThread();
// 2.拿到当前线程Thread 内部的ThreadLocalMap容器
ThreadLocalMap map = getMap(t);
// 3.最后就把变量副本给丢进去。
if (map != null)
map.set(this, value);
else
createMap(t, value); // map不存在就创建,并把值丢进去
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
当我们在Thread内部调用set方法时:
- 第一步会去获取当前线程Thread
- 然后拿到当前线程Thread 内部的ThreadLocalMap容器
- 最后就把变量副本给丢进去。
没了…懂了吗,ThreadLocal(就是个维护线程内部变量的工具!)只是在Set的时候去操作了Thread内部的ThreadLocalMap,将变量拷贝到Thread内部的Map容器中,Key:当前的ThreadLocal,Value:变量的副本
⭐️ get方法
public T get() {
Thread t = Thread.currentThread();
// 1.获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 2.从map中根据this(当前的threadlocal对象)获取线程存储的Entry节点。
ThreadLocalMap.Entry e = map.getEntry(this);
// 3.从Entry节点获取存储的对应Value副本值返回。
if (e != null)
return (T)e.value;
}
return setInitialValue(); // map为空的话返回初始值null,即线程变量副本为null
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
T value = initialValue(); // map为空的话返回初始值null,即线程变量副本为null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// map为空的话返回初始值null,即线程变量副本为null。
protected T initialValue() {
return null;
}
⭐️ 设置初始值
private T setInitialValue() {
// 1. initialValue():protected方法,默认返回 null。典型用法中常常重载该方法
T value = initialValue();
Thread t = Thread.currentThread();
// 2. 拿到该线程Thread对应的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 3.map不为null,该ThreadLocal对象与对应实例初始值的映射添加进该线程的 ThreadLocalMap中
if (map != null)
map.set(this, value);
// 4.map为null,先创建该 ThreadLocalMap 对象再将映射添加其中。
else
createMap(t, value);
return value;
}
无需考虑 ThreadLocalMap 的线程安全问题:因为每个线程有且只有一个 ThreadLocalMap 对象,并且只有该线程自己可以访问它
▎ThreadLocal 常见面试题
① 什么是ThreadLocal? 用来解决什么问题的?
ThreadLocal是一个:在多线程中为每一个线程创建单独的变量副本 的类;解决了变量在线程之间隔离,而在方法或类间共享的场景
② 说说你对ThreadLocal的理解?
ThreadLocal 提供了线程本地的实例。 它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static
修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。由于在每个线程中都创建了副本,故要考虑对资源的消耗,比如内存的占用会比不使用要大
ThreadLocal就是提供给每个线程操作变量的工具类,做到了线程之间的变量隔离目的
③ ThreadLocal是如何实现线程隔离的?
主要用到了线程Thread 中的 ThreadLocalMap,每个线程有且只有一个ThreadLocalMap,且只有当前线程能够访问它,故此就实现了隔离
④ ThreadLocal是怎么把变量复制到Thread的ThreadLocalMap中的?
当我们初始化一个线程时,其内部就去创建了一个ThreadLocalMap的Map容器待用
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
ThreadLocalMap 被创建加载时,其静态内部类Entry也随之加载,完成初始化动作:
public class ThreadLocal<T> {
...
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
⑤ 那么它又是如何和ThreadLocal缠上关系,ThreadLocal又是如何管理键值对的关系
- set()方法用于保存当前线程的副本变量值。
- get()方法用于获取当前线程的副本变量值。
- initialValue()为当前线程初始副本变量值。
- remove()方法移除当前线程的副本变量值。
set 方法解析:
public class ThreadLocal<T> {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}
当我们在Thread内部调用set方法时:
- 第一步会去获取调用当前方法的线程Thread
- 然后拿到当前线程内部的ThreadLocalMap容器
- 最后就把变量副本给丢进去。
没了…懂了吗,ThreadLocal(就是个维护线程内部变量的工具!)只是在Set的时候去操作了Thread内部的ThreadLocalMap,将变量拷贝到Thread内部的Map容器中,Key:当前的ThreadLocal,Value:变量的副本
get 方法解析:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
- 获取当前线程的ThreadLocalMap对象
- 从map中根据this(当前的threadlocal对象)获取线程存储的Entry节点。
- 从Entry节点获取存储的对应Value副本值返回。
- map为空的话返回初始值null,即线程变量副本为null。
⑥ 既然ThreadLocal只是个维护线程内部变量的工具,为什么不直接用ThreadLocalMap?
ThreadLocalMap属于ThreadLocal的静态内部类,map的方法都在ThreadLocal类中调用
⑦ ThreadLocalMap使用ThreadLocal的弱引用作为key,为什么不使用强引用?
-
key强引用:实际开发中,不需要ThreadLocal了,如果线程仍在运行,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal 就不会被GC回收,易发生内存泄漏
内存泄漏:本该被回收的对象不能被回收,而停留在堆内存中占有内存(那块内存也不能再访问)
-
key弱引用:弱引用就不存在此问题,由于ThreadLocalMap持有的ThreadLocal是弱引用,即使没有手动删除,也会被GC回收(key = null);使用弱引用对于 threadlocal 对象而言是不会发生内存泄漏的。
Java8中,ThreadLocal的get()、set()、remove()方法调用时,会清除掉线程ThreadLocalMap中所有Entry中 Key为null的Value,并将整个Entry设置为null,利于下次内存回收。(前提是key是弱引用)
虽key被回收了, 但ThreadLocalMap的value还是强引用,只有thead线程退出以后,value的强引用链条才会断掉(但如果是线程池呢?线程没有销毁,不会被回收,Value一直存在:内存泄漏)
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用
⑧ 降低内存泄漏的风险
Java8中,ThreadLocal的get()、set()、remove()方法,会清除掉线程ThreadLocalMap中所有Entry中 Key为null的Value,并将整个Entry设置为null,故当线程使用完ThreadLocal后,建议手动调用remove方法清除
⑨ 还有哪些使用ThreadLocal的应用场景?
- 每个线程需要有自己单独的实例 (也可线程内部构建单独的实例,但ThreadLocal更方便)
- 实例不被多线程共享,但需在多个方法中共享(可通过方法间形参传递实现,但ThreadLocal降低耦合)
⑩ 为什么JDK建议ThreadLocal定义为 static?
-
static 避免重复创建实例,浪费内存
被static修饰为类变量,该类的所有实例都共享此变量 ,类加载时就完成了内存的分配和初始化在内存中只有一个副本,所有此类的实例对象(该线程内定义的)都可以操控这个变量。从而避免创建重复,造成内存浪费。
避免重复创建ThreadLocal所关联的对象:ThreadLocal<Cat> local = new ... 中的 Cat 对象
-
当static时,延长ThreadLocal 生命周期,避免被回收
ThreadLocalMap的key是弱引用,如外部未强引用ThreadLocal,会被系统GC回收,key为null无法访问value,线程不结束,无用的value无法回收造成内存泄漏。static保证ThreadMap的key存在强引用,线程生命期内始终有值,而不被回收,从而通过ThreadLocal的弱引用访问到Entry中的value值。如在线程池下,不用ThreadLocal了,必须 (1) remove 或 (2) 手动 ThreadLocal ref = null
ThreadLocal 内存泄漏的原因:
实心箭头表示强引用,空心箭头表示弱引用
从上图中可以看出,threadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
HashCode 计算
ThreadLocalMap中没有采用传统的调用ThreadLocal的hashcode方法(继承自object的hashcode),而是调用nexthashcode,源码如下:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
//1640531527 能够让hash槽位分布相当均匀
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
Hash冲突
ThreadLocalMap解决Hash冲突的方式:简单的步长加1或减1及线性探测,寻找下一个相邻的位置
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如有大量不同的ThreadLocal对象放入map中时容易发生冲突。
建议每个线程只存一个变量(一个ThreadLocal)就不存在Hash冲突的问题,如果一个线程要保存set多个变量,则需创建多个ThreadLocal,但也易增加Hash冲突的可能。