TheadLocal介绍及用法
ThreadLocal是线程的本地变量。当使用ThreadLocal维护变量时,它为每个线程提供独立的变量副本。
每一个线程可以独立地操作自己的变量,不受其他线程影响。
使用场景
- 作为数据副本,当某些数据是以线程为作用域并且不同线程有不同数据副本,使用ThreadLocal。
- 保存线程上下文信息,在任意需要的地方可以获取,避免显示传参。
- 解决线程安全问题,避免某些情况需要考虑线程安全必须同步带来的性能损失。
ThreadLocal与Synchronized
ThreadLocal是与线程绑定的一个变量,其与Synchronized都用于解决多线程并发问题。
但Synchronized设计用于线程间变量共享;而ThreadLocal用于线程间变量隔离。
Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象。
Thread实现原理与内部结构
ThreadLocal
ThreadLocal由两个部分组成,ThreadLocal与ThreadLocalMap,后者为前者的一个静态内部类。
ThreadLocal的核心方法有三个:set(), **get()和remove()**。
ThreadLocalMap
ThreadLocalMap(简称TLM),TLM是ThreadLocal的核心。本质上,TLM是一个定制化的HashMap。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
它继承了弱引用(WeakReference),键是ThreadLocal本体,值就是我们存入的value。在默认情况下, 每个线程中的这两个变量都为null。
在创建Map时,传入的Thread的内部成员变量会引用该TLM。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
方法解析
set()
public void set(T value) {
// 1.获取调用线程(当前线程)
Thread t = Thread.currentThread();
// 2.寻找当前线程的TLM
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3.1如果TLM存在,则直接set
map.set(this, value);
} else {
// 3.2如果TLM不存在,就新建TLM
createMap(t, value);
}
}
getMap()方法:
返回当前线程引用的TLM。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
get()
public T get() {
// 1.获取当前线程
Thread t = Thread.currentThread();
// 2.获取TLM
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
setInitialValue():
private T setInitialValue() {
// 1.创建默认值,实际上就是null
T value = initialValue();
// 2-3.获取线程和TLM
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 4.根据map不同情况执行不同操作
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
// JDK11新增了TerminatingThreadLocal类,主要用于解决内存泄露问题。
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
remove():
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
本质
由以上分析我们可以知道,本地变量实际存放在调用线程的threadLocals,ThreadLocal只是一个封装壳。
ThreadLocal的核心是TLM。
ThreadLocal内存泄露问题
名词解释:
内存溢出(Out of Memory)
内存溢出是指程序在申请内存时,没有足够的内存供其使用。
内存泄露(MemoryLeak)
内存泄露是指程序在申请内存后,无法或者未释放已经申请到的内存。
MemoryLeak积少成多,会导致Out of Memory。
强引用与弱引用
TLM的Entry显式继承了WeakRereference的ThreadLocal。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
引用类型一般在java.lang.ref包下,常见的引用有:FinalReference, SoftReference以及WeakReference。
强引用(StrongReference)
StrongReference在上述包下并没有实际的对应类,但我们程序中几乎所有的引用使用的都是强引用。
例如
StringBuilder sb = new StringBuilder();
// 这里栈上的sb对堆上的StringBuilder对象是强引用
强引用可以直接访问目标对象,并且指向的对象任何时候都不会被回收,即使不回收该对象会抛出OOM(OutOfMemory),JVM也不会回收。
弱引用(WeakReference)
弱引用中的对象具有很短的声明周期,因为在系统GC时,只要发现弱引用,不管堆空间是否足够,都会将对象进行回收。由于垃圾回收器是一个优先级很低的线程,因此不一定立即发现那些只具有弱引用的对象。
问题原因
ThreadLocal是WeakReference,如果一个ThreadLocal没有外部强引用来引用它,就会被GC回收。
ThreadLocalMap中就会出现key为null的Entry,key为null,则无法访问这些Entry的value。
而在线程池技术下,线程经常服用,生命周期非常长,甚至与JVM共生。
这样一条引用链会一直保持:Thread Ref-> Thread -> ThreaLocalMap -> Entry -> value。
这会导致value无法被回收,引起内存泄露。
ThreadLocal为什么要弱引用
如果使用强引用的话
ThreadLocalMap的生命周期基本和Thread的生命周期一样,当前线程如果没有终止,那么ThreadLocalMap始终不会被GC。ThreadLocalMap持有对ThreadLocal的强引用,那么ThreadLocal也不会被GC,当线程生命周期长,如果没有手动删除,则会造成Entry的累积,从而导致OOM。
从另一个方面来说,强引用只是在GC层面将内存泄露的问题掩盖起来,并没有真正解决问题。
由上可知:引起内存泄露的根本原因不是弱引用,而是因为TLM与Thread共生,即TLM生命周期和Thread保持一致。
所以,在使用时一定要使用完ThreadLocal后调用remove()
方法清理。