ThreadLocal 本地线程变量详解
概述
ThreadLocal 意为本地线程变量,即该变量只属于当前线程,对其他线程隔离
我们知道,一个普通变量如果被多线程访问会存在存在线程安全问题,这时我们可以使用 Synchronize 来保证该变量某一时刻只能有一个线程访问,从而解决并发安全问题
但如果这个变量并不需要被共享,那么就可以使用 ThreadLocal 为每个线程提供一个完全独立的变量副本,每个线程只操作自身拥有的副本,彼此互不干扰
简而言之,Synchronized 用于线程间的数据共享,同步机制采用采用时间换空间的方式,而 ThreadLocal 则用于线程间的数据隔离,采用空间换时间的方式
ThreadLocal 使用
public class ThreadLocalTest {
// 有个User对象需要在不同线程之间进行隔离访问,可以定义ThreadLocal如下
static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
// 设置线程本地变量的内容
public void setUser(User user) {
userThreadLocal.set(user);
}
public void getUser() {
// 获取线程本地变量的内容
User user = userThreadLocal.get();
user.setUsername(user.getUsername + "-" + Thread.currentThread().getName());
System.out.println(user.getUsername());
// 移除线程本地变量
userThreadLocal.remove();
}
}
测试代码如下
public class DemoTest {
public static void main(String[] args) {
ThreadLocalTest threadLocalTest = new ThreadLocalTest();
for(int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
User user = new User();
user.setUsername("小明");
threadLocalTest.setUser(user);
threadLocalTest.getUser();
});
thread.setName(String.valueOf(i));
thread.start();
}
}
}
ThreadLocal 原理
首先,Java 中的线程是一个 Thread 类的实例对象,对象可以定义私有的成员变量,这也是 ThreadLocal 能实现线程本地变量的基础
在 Thread 类中定义了一个 Map 类型的成员变量,用来保存该线程的所有本地变量
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 的 Entry 的定义如下,key 为 ThreadLocal 对象,v 就是我们要在线程之间隔离的对象
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal::set 方法的源码如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
使用 set 方法赋值时,首先会获取当前线程 thread,并获取 thread 线程的 ThreadLocalMap 属性。如果 map 属性不为空,则直接更新 value 值,key 就是自身的 ThreadLocal,如果 map 为空,则实例化 threadLocalMap,并将 value 值初始化
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) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
使用 get 方法获取值,也是首先获取当前线程,再获取线程 ThreadLocalMap,如果 map 不为空就用自身 ThreadLocal 为 key 从 map 获取对应的 value
ThreadLocal::remove 方法的源码如下:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
remove 方法也是获取当前线程并获取 ThreadLocalMap,再将自身 ThreadLocal 为 key 对应的 value 移除
综合以上,我们知道 ThreadLocalMap 是线程的一个属性值,用来保存该线程的本地变量。ThreadLocal 能操作当前线程的 ThreadLocalMap,具体做法是以自身为 key 在 map 中存取值。因为每个线程的 ThreadLocalMap 都是独立的,所以每次使用 ThreadLocal 存取值都仅限于当前的线程,不会影响其他线程
ThreadLocal 内存泄露问题
内存泄露问题:指程序中动态分配的堆内存由于某种原因没有被释放或者无法释放,造成系统内存的浪费,导致程序运行速度减慢或者系统奔溃等严重后果。内存泄露堆积将会导致内存溢出
观察使用 ThreadLocal 时的内存布局
当 ThreadLocal Ref 被回收了,由于在 Entry 使用的是强引用,在 Current Thread 还存在的情况下就存在着到达 Entry 的引用链,无法清除掉 ThreadLocal 的内容,同时 Entry 的 value 也同样会被保留,也就是说使用了强引用会出现内存泄露问题
为此,ThreadLocal 在 Entry 使用了弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
// Entry继承WeakReference,将key传入父级构造方法,从而形成弱引用
super(k);
value = v;
}
}
再观察使用软引用后的内存布局
当 ThreadLocal Ref 被回收,由于在 Entry 使用的是弱引用,因此在下次垃圾回收的时候就会将 ThreadLocal 对象清除
但由于 ThreadLocalMap 仍然存在 Current Thread Ref 这个强引用,Entry 中 value 的值仍然无法清除,还是存在内存泄露的问题,虽然当线程生命周期结束,Current Thread Ref 这个强引用也会随之消失,value 会在下一次垃圾回收被清除,但如果使用线程池,那么线程会被重新放回线程池等待复用,那么强引用就会一直存在。因此,我们在每次在使用完之后需要手动的 remove 掉 Entry 对象