学习笔记:ThreadLocal使用及注意事项
2020-07-01 本文已影响0人
大力papa
本文仅供学习交流使用,侵权必删。
不作商业用途,转载请注明出处。
自JDK1.2版本起,java提供了java.lang.ThreadLocal。ThreadLocal为使用该变量的线程都提供了相互独立的副本,实现线程间的数据隔离。
ThreadLocal的使用场景和源码分析
- 在对象跨层传递的时候,可以使用ThreadLocal,避免方法多次参数传递
- 进行事务操作,用于存储线程事务信息。Spring中的AbstractRoutingDataSource实现动态数据源切换就会用到ThreadLocal
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> tl = new ThreadLocal();
ThreadLocal<String> tl2 = new ThreadLocal();
new Thread(() -> {
tl.set("t1_threadlocal");
tl2.set("t1_threadlocal_2");
System.out.println("t1: " + tl.get());
System.out.println("t1: " + tl2.get());
System.out.println("t1: " + tl.get());
tl.remove();
}, "t1").start();
new Thread(() -> {
tl.set("t2_threadlocal");
System.out.println("t2: " + tl.get());
System.out.println("t2: " + tl2.get());
}, "t2").start();
}
initialValue()
initialValue()为ThreadLocal要存储的数据类型指定一个初始化值,不重写该方法默认是返回null
public static void main(String[] args) {
ThreadLocal<String> tl = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "hello";
}
};
new Thread(()->{
System.out.println("tl::get : "+tl.get());
}).start();
//返回我们定义的初始化值hello
}
set(T value)
set方法把当前ThreadLocal和我们需要设置的值绑定设置到当前线程的ThreadLocalMap中
ThreadLocal::set
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//从当前线程中获取ThreadLocalMap
if (map != null)
map.set(this, value);//调用ThreadLocalMap set方法
else
createMap(t, value);//否则创建ThreadLocalMap并set
}
ThreadLocalMap::set
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
/*
寻找当前ThreadLocal是否已经设置过value
*/
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//Entry获得的ThreadLocal等于当前ThreadLocal
if (k == key) {
e.value = value;
return;
}
/*
Entry里面的ThreadLocal为null
有可能是ThreadLocal已经没有被强引用指向,gc已经把它回收了
*/
if (k == null) {
//替换旧的Entry,key为当前ThreadLocal
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
/*
清理ThreadLocal为null的Entry,value指向null,防止memory leak
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocal::createMap
void createMap(Thread t, T firstValue) {
// 初始化ThreadLocalMap,并把firstValue设置到Map中
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 获取当前线程以及当前线程对象指向的ThreadLocalMap
- 如果ThreadLocalMap不为null,调用第3步 ThreadLocalMap的set方法。否则调用第4部 createMap
- ThreadLocalMap的set方法
- 遍历查询当前线程的ThreadLocalMap的Entry[],如果找到对应的Entry对象且通过Entry::get获取的ThreadLocal跟当前的ThreadLocal相等,那么就将新值替代旧值。如果Entry::get为null,直接将其逐出并使用新值占用被逐出数据的位置
- 如果从Entry[]数组中没有遍历查询到对应的Entry对象,则创建一个新的Entry,使用ThreadLocal为key,要存放的值为value。存储到数组中
- createMap,为当前线程创建一个ThreadLocalMap,并将当前需要存放的值存储进去
get()
ThreadLocal::get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取线程中的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
/*
如果当前线程的ThreadLocal的Entry为null,
或者ThreadLocalMap未初始化,
就调用setInitialValue,返回一个初始值
*/
return setInitialValue();
}
}
ThreadLocalMap::getEntry
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
1.根据当前线程获取ThreadLocalMap,如果map为null,调用 第2步,否则调用第3步
2.调用setInitialValue返回一个初始值
3.调用ThreadLocalMap的getEntry方法获取对应的值
ThreadLocal内存泄漏问题
image.png image.png- 图一是正常引用了ThreadLocal变量的图,而当ThreadLocalRef显式地指向null时,就成为图二的情况。gc就会将heap中的ThreadLocal回收。Entry.key就会为null,无法获取到value,但ThreadRef到Entry.value的引用链路是可达的。所以Entry.value 一直不会被gc回收而导致内存泄漏。
- ThreadLocal内部针对内存泄漏问题是实现了相应的解决方案的。
- ThreadLocal的set方法里面cleanSomeSlots调用了expungeStaleEntry清除key为null的Entry
- ThreadLocal的get方法里面getEntry调用了getEntryAfterMiss方法,这个方法同样调用了expungeStaleEntry清除key为null的Entry
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);//清除key为null的Entry
}
} while ((n >>>= 1) != 0);
return removed;
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
- 但即便ThreadLocal有相应的处理,其实也会有可能存在内存泄漏的情况。当程序既不调用get,又不调用set的情况下就触发不了ThreadLocal的处理机制。所以日常开发中,当使用完ThreadLocal的时候调用一下remove()
总结
ThreadLocal的用法相对简单,但由于有可能出现内存泄漏的情况,所以开发过程中需要注意。