- 是什么?
- 能干嘛?
- 常用API
- 案例一
- 以上代码存在的问题?
- 演示线程池复用本地变量的情况
- ThreadLocal源码
- Thread,ThreadLocal,ThreadLocalMap 三者的关系?
- ThreadLocal 的 get 方法
- set、remove 方法
- 总结
- ThreadLocal 之内存泄漏
- 四大引用
- ThreadLocal为什么要使用弱引用
- ThreadLocal使用弱引用就没有问题了吗?
- 总结
是什么?
官网描述:
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
能干嘛?
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
在之前多个线程共享同一个数据就有可能存在数据安全问题,因此使用 synchronized、lock/unlock、CAS 保证安全
而ThreadLocal可以让每个线程都有一份数据,这样就避免了多个线程同时操作同一个数据而带来的线程安全问题。
常用API
构造器:
Constructor and Description |
---|
ThreadLocal() 创建线程局部变量。 |
常用方法:
Modifier and Type | Method and Description |
---|---|
T | get() 返回当前线程的此线程局部变量的副本中的值。 |
protected T | initialValue() 返回此线程局部变量的当前线程的“初始值”。 |
void | remove() 删除此线程局部变量的当前线程的值。 |
void | set(T value) 将当前线程的此线程局部变量的副本设置为指定的值。 |
static <S> ThreadLocal<S> | withInitial(Supplier<? extends S> supplier) 创建线程局部变量。 |
案例一
通过一个案例熟悉使用 ThreadLocal
某房地产公司要求统计销售员总共卖出的房子(假设有5个销售员)
代码:
public class SaleHouseDemo {
public static void main(String[] args) {
SaleHouse house = new SaleHouse();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5);
System.out.println(Thread.currentThread().getName() + " 卖出 " + size);
for (int i1 = 0; i1 < size; i1++) {
house.saleHouse();
}
}, String.valueOf(i)).start();
}
try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("总共卖出: " + house.saleCount);
}
}
// 资源类
class SaleHouse {
int saleCount = 0 ;
public synchronized void saleHouse() {
++saleCount;
}
}
5 卖出 4
3 卖出 4
1 卖出 2
2 卖出 4
4 卖出 3
总共卖出: 17
此时修改一下需求,不参与统计,每个销售员自己算自己的。
就需要使用 ThreadLocal 为每一个线程绑定属于自己的变量。不需要别的线程干涉。
ThreadLocal 初始化变量的俩种方式:
- 重写 initialValue 方法
- 不推荐这种方式,太繁琐。
// 原始版本,不推荐
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0; // 变量的初始值为0
}
};
- 调用静态方法 withInitial
// 调用静态方法: 推荐
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
代码演示:
public class SaleHouseDemo {
public static void main(String[] args) {
SaleHouse house = new SaleHouse();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5);
for (int i1 = 0; i1 < size; i1++) {
house.saleHouseByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + " 卖出 " + house.threadLocal.get());
}, String.valueOf(i)).start();
}
try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
// System.out.println("总共卖出: " + house.saleCount);
}
}
// 资源类
class SaleHouse {
int saleCount = 0 ;
public synchronized void saleHouse() {
++saleCount;
}
// 原始版本,不推荐
// ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
// @Override
// protected Integer initialValue() {
// return 0;
// }
// };
// 调用静态方法: 推荐
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void saleHouseByThreadLocal() {
threadLocal.set(1 + threadLocal.get());
}
}
5 卖出 1
2 卖出 3
1 卖出 1
4 卖出 1
3 卖出 4
以上代码存在的问题?
在使用完本地变量后,要记着使用 remove 方法清除。否则会造成 内存泄漏
的问题。
其实在以上场景中,你会发现并没有出现问题,因为我们每次创建线程都是新 new 的,线程也没有重复使用。但是在线程池的场景下,就可能出现问题了,请看阿里巴巴规范的说明。
阿里巴巴规范:
当使用完 本地变量,要在 finally 语句块里清除。
修改以上代码:
演示线程池复用本地变量的情况
阿里规范中说明:如果本地变量使用完未清除,在线程池的场景下,很可能会出现重复利用的问题。
演示代码:
public class ThreadLocalTest {
public static void main(String[] args) {
Resource resource = new Resource();
ExecutorService executorService = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
Integer begin = resource.threadLocal.get();
resource.add();
Integer end = resource.threadLocal.get();
System.out.println(Thread.currentThread().getName() + " begin = " + begin + " end = " + end);
});
}
} finally {
executorService.shutdown();
}
}
}
class Resource {
int num = 0;
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void add() {
threadLocal.set(1 + threadLocal.get());
}
}
输出结果:
pool-1-thread-1 begin = 0 end = 1
pool-1-thread-3 begin = 0 end = 1
pool-1-thread-2 begin = 0 end = 1
pool-1-thread-1 begin = 1 end = 2
pool-1-thread-2 begin = 1 end = 2
pool-1-thread-3 begin = 1 end = 2
pool-1-thread-2 begin = 2 end = 3
pool-1-thread-1 begin = 2 end = 3
pool-1-thread-2 begin = 3 end = 4
pool-1-thread-3 begin = 2 end = 3
在每次执行的任务中,有的任务重复利用了之前任务的变量值,这很有可能影响后序的逻辑,因此在使用完本地变量时,要记着清除~!
执行结果:
ThreadLocal源码
Thread,ThreadLocal,ThreadLocalMap 三者的关系?
线程类 Thread 中存在有 ThreadLocalMap的属性,每次新建Thread 都会有一个新的 ThreadLocalMap, 这就是为什么 每个线程独有一份 本地变量
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,并且静态内部类Entry 继承了 WeakReference 弱引用
三者关系图:
- threadLocalMap实际上就是一个以 threadLocal实例为key,任意对象为value的Entry对象。
- 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
如果对这俩句话还是不懂的话,继续往下看源码。
ThreadLocal 的 get 方法
get 方法用于获取当前线程本地变量的副本
public T get() {
// 首先获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap,具体的获取方法就是调用Thread类中的 threadlocals 属性。参考图1
ThreadLocalMap map = getMap(t);
if (map != null) {
// 如果不为空,就获取 ThreadLocal对象的 Entry 对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 如果 e 不为null,就通过Entry对象获取value,也就是本地变量的值。
T result = (T)e.value;
return result;
}
}
// 如果 map = null ,也就是在我们创建 ThreadLocal 没有进行初始化。会执行这个方法进行初始化
return setInitialValue();
}
初始化 ThreadLocalMap
private T setInitialValue() {
// 参考图二:对本地变量进行初始化 null ,这就是为什么说在创建ThreadLocal时一定要初始化变量,否则变量值就是 null
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// map 不为空,将当前对象(ThreadLocal)作为key,本地变量值作为 value 增加到map中
map.set(this, value);
else
// 如果map为空,就新创建一个Map,参考图三
createMap(t, value);
return value;
}
创建一个 ThreadLocalMap,仍然是以 ThreadLocal对象为 key,本地变量值为 value
set、remove 方法
弄清楚了 get() 方法,set和 remove 方法就很好懂了。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
总结
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:
JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中), 每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
ThreadLocal 之内存泄漏
阿里巴巴的规范手册中说明:
如果使用完本地变量后,不进行回收,可能会造成内存泄漏
为什么会造成内存泄漏呢?谁造成的呢?
正是存入 ThreadLocalMap 中的 Entry 对象。
回顾一下ThreadLocalMap :
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个 弱引用对象;
(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>
四大引用
关于什么是 四大引用,我在 JVM 篇章中也有过介绍,这里不做多说明。包括 对象的finalization机制
弱引用和ThreadLocal的关系
每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储:
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
ThreadLocal为什么要使用弱引用
看一下这个ThreadLocal代码:
public void function01() {
ThreadLocal<Object> tl = new ThreadLocal<>();
tl.set(1);
tl.get();
}
对应的内存关系图:
局部变量 tl 会保存虚拟机栈中,指向堆中的 THreadLocal 的对象,这就是强引用。
而在执行 set 方法时,该线程的 ThreadLocalMap 的key 会指向ThreadLocal 对象。
- 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
- 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
ThreadLocal使用弱引用就没有问题了吗?
虽然 ThreadLocal 使用了若引用减少了内存泄漏的几率。但还可能会存在内存泄漏。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。
虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
总结起来就俩句话:
- ThreadLocalMap使用 ThreadLocal的弱引用作为 key,当 ThreadLocal没有外部引用 指向它的时候,能够被 GC 回收,也就是 将 key 设置为 null
- 虽然 ThreadLocal 能被回收,但是 value 还是存在的(和HashMap一样,允许key为null 存在)。因此使用弱引用不能百分百保证不会出现内存泄漏,还需要在使用完 ThreadLocal 后调用remove方法清除。
在上面说,set、get方法会去检查所有键为null的Entry对象,那么在源码中时如何体现的?
get方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hd5d12xm-1669802720879)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/202211301804535.png)]
getEntryAfterMiss 调用了 expungeStaleEntry 方法,在此方法中,将value设置为了null
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1FclbVe-1669802720880)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/202211301804536.png)]
set 方法:
remove 方法:
同样调用了expungeStaleEntry 方法
总结
从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。
总结
ThreadLocal 使用建议:
- 一定要进行初始化避免空指针问题ThreadLocal.withInitial(()- > 初始化值); 【强制】
- 建议把ThreadLocal修饰为static【建议】
- 用完记得手动remove 【强制】
阿里巴巴规范:
ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。
所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。
- ThreadLocal 并不解决线程间共享数据的问题
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,
- 该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
- 都会通过expungeStaleEntry, cleanSome Slots,replaceStaleEntry这三个方法回收键为 null 的 Entry
.(img-CkDpd1kt-1669802720882)]
ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。
所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。
各位彭于晏,如有收获点个赞不过分吧…✌✌✌
gongzhonghao 回复 [JUC] 获取MarkDown笔记