Java ThreadLocal原理
为什么要有ThreadLocal
多线程利用锁来解决共享变量的问题,那如果每个线程都有自己的一份变量,就不涉及到线程安全的事情了。 当然这个变量不是方法里线程栈上的变量,是整个线程级别都可以访问的变量。
ThreadLocal出现就能解决这个问题,那它是怎么实现的呢?
看一段代码
每个线程有自己的threadlocal值
public class Test {
/**
* static final修饰 threadlocal变量
* 每个线程里的ThreadLocalMap.Entry,key所有线程都是这一个变量,value各线程有各自的值
*/
private static final ThreadLocal<String> threadlocalStr = ThreadLocal.withInitial(() -> "xx");
public static void main(String[] args) throws InterruptedException {
int threadNum = 4;
// 4个线程
ExecutorService executorService = Executors.newFixedThreadPool(threadNum);
for (int i = 0; i < threadNum; i++) {
final int j = i;
// 每个线程写自己的threadlocalStr
executorService.submit(() -> {
Test.threadlocalStr.set(Thread.currentThread().getName() + "-" + j);
});
}
TimeUnit.SECONDS.sleep(2);
for (int i = 0; i < threadNum; i++) {
executorService.submit(() -> {
// 每个线程读自己的threadlocalStr
String s = Test.threadlocalStr.get();
System.out.println(Thread.currentThread().getName() + " ==== " + s);
});
}
}
}
// threadLocal.get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal 原理
类引用关系
Thread -> ThreadLocal.ThreadLocalMap -> ThreadLocal.ThreadLocalMap.Entry[] -> ThreadLocal.ThreadLocalMap.Enrty -> key(threadLocal对象,是个弱引用)和value
类简要代码
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
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;
}
}
return setInitialValue();
}
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;
}
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;
}
}
}
}
ThreadLocal 与弱引用
上述代码能看到 Entry构造函数中,弱引用即是ThreadLocal这个变量。
又根据上述类引用关系,知道是线程类Thread一步一步引用着 ThreadLocal,如果这个线程不结束(池化的线程通常不结束),那这个引用包括Entry里的value是不是造成内存泄露了呢?
弱引用
弱引用有什么特点?
弱引用对象被回收,是说这个弱引用对象只有,注意是只有,被弱引用引用着,才会在gc时候给回收。如果还在被强引用着,就不会回收。注意这个点。
来一段代码理解下弱引用:
public static void main(String[] args) {
// 强引用
Person person = new Person("Peter");
// 弱引用
WeakReference<Person> weakReference = new WeakReference<>(person);
// 输出: Person(name=Peter)
System.out.println(weakReference.get());
// 解除强引用
person = null;
// 输出: Person(name=Peter)
// WeakReference的referent拿到的是person对象的地址,再让person指向null 也不影响referent的指向
System.out.println(weakReference.get());
// gc
// 如果person = null打开,表示解除person的强引用,这时候person只有弱引用在用,需要回收掉person
// 如果注释掉,那么person还有强引用在用,不会回收person
System.gc();
// 打开 person = null,因为person被gc 则输出null
// 注释 person = null,因为还有强引用不会gc 则输出: Person(name=Peter)
System.out.println(weakReference.get());
}
@Data
@AllArgsConstructor
class Person {
private String name;
}
ThreadLocal 为什么设计成弱引用呢?
上边理解了弱引用,这里也就能明白。
假设是强引用:一个对象内部持有一个threadLocal(这个threadLocal既被这个对象强引用,又被当前线程强引用),当这个对象变成垃圾以后,threadLocal就只被线程强引用了。 那如果线程不结束,这个threadLocal就一直不会被回收,造成内存泄露。
如果是弱引用,那么当宿主对象成垃圾时,threadLocal就只剩弱引用了,那么当GC时,它就可以被回收。
这还会有另一个问题,就是key被回收了,但是value还是被Entry强引用着,会无法回收。
那么value咋不设置弱引用呢?假设value是弱引用,当外部(非Thread类下)没有引用value时,那GC时value就没了,会导致ThreadLocal无法使用。
ThreadLocal 为什么能造成内存泄露
当作为key的threadLocal被回收了,但是value还是在被Entry对象强引用,线程不结束还是会内存泄露。
这也就是为什么建议我们使用完主动remove()的原因。
ThreadLocal 为什么建议用static修饰
ThreadLocalMap下包含的是Entry[]数组,当然一个线程支持多个threadLocal对象。那为什么建议用static修饰呢?
如果是static类级别的修饰,并且能达到这个ThreadLocal应用场景下的要求,那么创建更少的threadLocal自然可以节约资源。
还有一点,ThreadLocal内部会在set()、get()时帮我们清除没用的Entry 减少内存泄露的问题,如果是static修饰只有一个threadLocal 会降低我们在使用上的风险。
ThreadLocal 典型应用场景
- 事务上下文里,把数据库连接放到ThreadLocal里
- Hikari连接池中,把连接放到线程ThreadLocal里,方便下一次获取
- 上下文环境全局参数,避免方法间参数传递