一、ThreadLocal 定义
官方JDK的定义:此类提供线程局部变量。这些变量与其正常对应变量的不同之处在于,每个访问一个(通过其get或set方法)的线程都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。
例如,下面的类生成每个线程本地的唯一标识符。线程的id在第一次调用ThreadId.get()时被分配,并且在随后的调用中保持不变。
ThreadLocal
是用来存放线程相关数据的一个容器,这个容器叫做ThreadLocalMap
,它是ThreadLocal
的一个静态内部类,同时作为Thread
类的一个成员变量。ThreadLocal
在使用时,先拿到当前线程的成员变量ThreadLocalMap
,以当前的ThreadLocal
对象作为key
,变量作为value
存入ThreadLocalMap
。 ThreadLocal
相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal
关联的实例互不干扰。然后每个线程取变量都是从线程各自的ThreadLocalMap
中取值,自然是线程安全的了。因为变量只在自己线程的生命周期内起作用,所以说ThreadLocal
提供线程局部变量,或者叫线程本地变量。
ThreadLocal
实例通常总是以静态字段初始化如下:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
二、ThreadLocal的使用
ThreadLocal 的常用方法:
public ThreadLocal()
:通过构造器创建对象。一般是静态的。static <S> ThreadLocal<S>withInitial(Supplier<? extends S> supplier)
:初始化一个 ThreadLcoal。void set(T value)
:设置当前线程绑定的局部变量。T get()
:返回此线程局部变量的当前线程副本中的值。void remove()
:删除当前线程绑定的局部变量。
对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:
public void process(User user) {checkPermission();doWork();saveStatus();sendResponse();
}
process()
方法需要传递的状态就是User
实例。
这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。
给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User
对象就传不进去了。
Java标准库提供了一个特殊的ThreadLocal
,它可以在一个线程中传递同一个对象。
ThreadLocal
实例通常总是以静态字段初始化如下:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
它的典型使用方式如下:
void processUser(user) {try {threadLocalUser.set(user);step1();step2();} finally {threadLocalUser.remove();}
}
通过设置一个User
实例关联到ThreadLocal
中,在移除之前,所有方法都可以随时获取到该User
实例:
void step1() {User u = threadLocalUser.get();log();printUser();
}void log() {User u = threadLocalUser.get();println(u.name);
}void step2() {User u = threadLocalUser.get();checkUser(u.id);
}
注意到普通的方法调用一定是同一个线程执行的,所以,step1()
、step2()
以及log()
方法内,threadLocalUser.get()
获取的User
对象是同一个实例。
实际上,可以把ThreadLocal
看成一个全局Map<Thread, Object>
:每个线程获取ThreadLocal
变量时,总是使用Thread
自身作为key:
Object threadLocalValue = threadLocalMap.get(Thread.currentThread());
因此,ThreadLocal
相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal
关联的实例互不干扰。
最后,特别注意ThreadLocal
一定要在finally
中清除:
try {threadLocalUser.set(user);...
} finally {threadLocalUser.remove();
}
这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal
没有被清除,该线程执行其他代码时,会把上一次的状态带进去。
为了保证能释放ThreadLocal
关联的实例,我们可以通过AutoCloseable
接口配合try (resource) {...}
结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的ThreadLocal
可以封装为一个UserContext
对象:
public class UserContext implements AutoCloseable {static final ThreadLocal<String> ctx = new ThreadLocal<>();public UserContext(String user) {ctx.set(user);}public static String currentUser() {return ctx.get();}@Overridepublic void close() {ctx.remove();}
}
二、ThreadLocal原理解析
ThreadLocal 的原理要从它的set(T value)
、get()、
remove()方法的源码入手:
1、set()方法
public void set(T value) {//获取当前线程Thread t = Thread.currentThread();//获取维护当前线程变量的ThreadLocalMap数据,一种类似于HashMap的数据结构ThreadLocalMap map = getMap(t);//如果当前线程已经存在了Map,直接调用map.setif (map != null)map.set(this, value);//不存在Map,则先进行新增map,再进行setelsecreateMap(t, value);
}
set方法中出现了一个ThreadLocalMap这个数据结构,点进去看一下
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}//代码太多不一一贴出来了
}
其中维护了一个entry结构用来用来维护节点的数据,细心地同学应该已经发现了Entry这个结构继承了WeakReference,从构造方法可以看出,ThreadLocalMap的Key是软引用维护的。这个地方很重要,至于为什么重要,后面再细说。
再继续点击一下发现ThreadLocal成员变量里面定义了这么一句话
ThreadLocal.ThreadLocalMap threadLocals = null;
这句话的出现表明了,针对于每一个线程,都是独立维护一个ThreadLocalMap,一个线程也可以拥有多个ThreadLocal变量。
2、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();
}
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}
protected T initialValue() {return null;
}
get()方法整体上比较简单,贴上了关键逻辑逻辑代码,调用get()时,如果存在值,则将值返回,不存在值调用setInitialValue()获取值,其中初始化的值为null,也就是说如果ThreadLocal变量未被赋值,或者赋值后被remove掉了,直接调用get()方法不会报错,将会返回null值。
3、remove()方法
//ThreadLocal
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}
//ThreadLocalMap
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}
}
remove方法调用时会判断当前线程中ThreadLocalMap是否存在,如果存在则调用ThreadLocalMap.remove(key);遍历链表结构移除entry节点
4、ThreadLocal 如何存多个变量
ThreadLocal 使用set
方法存数据时,key 用的this
对象,就是当前正在使用的 ThreadLocal 对象,说明一个 ThreadLocal 对象,在一个线程中,只能存一个线程本地变量。多个线程虽然都是用的是一个 key,但是不同的线程用的是不同的ThreadLocalMap
。
具体做法有两种:
1、 生成ThreadLocal 对象,每个 ThreadLocal 对象对应一个业务变量
2、给 ThreadLocal 初始化一个HashMap
,这是最常规的做法。比如下面:
public class ThreadLocalTest {private static final ThreadLocal<Map<String, Object>> context =ThreadLocal.withInitial(HashMap::new);private String getUserId() {return String.valueOf(context.get().get("userId"));}private void setUserId(String userId) {context.get().put("userId", userId);}public void setUserName(String userName) {context.get().put("userName", userName);}public String getUserName() {return String.valueOf(context.get().get("userName"));}public static void main(String[] args) {ThreadLocalTest test = new ThreadLocalTest();for (int i = 1; i < 5; i++) {Thread thread = new Thread(() -> {String threadName = Thread.currentThread().getName();test.setUserId(threadName + "的userId");test.setUserName(threadName + "的userName");System.out.println("===执行业务代码===");System.out.println(threadName + "-->" + test.getUserId() + "," + test.getUserName());});thread.setName("线程" + i);thread.start();}}
}