一、Thread-Local变量
1、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<>();
2、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();
}
@Override
public void close() {
ctx.remove();
}
}
3、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.set
if (map != null)
map.set(this, value);
//不存在Map,则先进行新增map,再进行set
else
createMap(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);
else
createMap(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();
}
}
}
二、继承Thread-Local变量
当父线程启动子线程时,父线程的线程局部变量的值都不会被子线程继承。但是,如果您希望子线程继承其父线程局部值的值,请使用Inheritable ThreadLocal类创建一个线程局部变量。
除了名为TLUSER的ThreadLocal变量外,以下示例还包括名为TLADMIN的Inheritable ThreadLocal变量:
public class TLDBConn {
final static ThreadLocal<User> TLUSER = new ThreadLocal<>();
final static InheritableThreadLocal<User> TLADMIN = new InheritableThreadLocal<>();
public static String open(String info) {
System.out.println(info + ": " + TLUSER.get().name);
return info + ": " + TLUSER.get().name;
}
}
以下方法在线程中启动一个名为childThread的线程。线程childThread检索名为TLADMIN的Inheritable ThreadLocal变量的值,并尝试检索名为TLSUER的ThreadLocal变量:
public void testConnectionWithInheritableTL(User u) {
Runnable r = () -> {
TLDBConn.TLUSER.set(u);
TLDBConn.TLADMIN.set(new User("Admin"));
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
System.out.println(TLServer.fetchOrder());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread childThread = new Thread(
() -> {
System.out.println("Child thread");
System.out.println("TLADMIN: " + TLDBConn.TLADMIN.get().name);
try {
System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);
} catch (NullPointerException e) {
System.out.println("NullPointerException: TLUSER hasn't beet set");
}
}
);
childThread.start();
TLDBConn.TLUSER.set(new User(u.name + " renamed"));
TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
};
Thread t = new Thread(r, u.name);
t.start();
}
当您调用此方法时,childThread实例化中的以下语句会抛出NullPointerException:
System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);