一、引言
1、背景
ThreadLocal的作用是提供线程内的局部变量,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。
2、作用
在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User
对象就传不进去了。
Java标准库提供了一个特殊的ThreadLocal
,它可以在一个线程中传递同一个对象。
二、使用
ThreadLocal
实例通常总是以静态字段初始化如下:
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
它的典型使用方式如下:
void processUser(user) {
try {
threadLocalUser.set(user);
step1();
step2();
} finally {
threadLocalUser.remove();
}
}
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);
}
ThreadLocal
相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal
关联的实例互不干扰。然而,特别注意ThreadLocal
一定要在finally
中清除:
try {
threadLocalUser.set(user);
...
} finally {
threadLocalUser.remove();
}
这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal
没有被清除,该线程执行其他代码时,会把上一次的状态带进去(详见第3章节)。
二、脏数据
1、原始错误代码
看如下代码及其运行结果:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocal01Test {
public static final ExecutorService executorService = Executors.newFixedThreadPool(2);
public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
executorService.execute(() -> {
THREAD_LOCAL.set("tom");
});
TimeUnit.SECONDS.sleep(1);
for (int i = 0; i < 10; ++i) {
executorService.execute(() -> {
String value = THREAD_LOCAL.get();
System.out.println("User:" + value);
});
}
}
}
代码运行结果如下:
User:tom
User:tom
User:null
User:tom
User:null
User:tom
User:null
User:tom
User:null
User:tom
我们首先在一个任务中设置了当前用户,然后在其后的10个任务中获取当前用户,但是发现有的获取到,有的为null,什么原因呢?
2、代码日志分析
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocal02Test {
public static final ExecutorService executorService = Executors.newFixedThreadPool(2);
public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
executorService.execute(() -> {
THREAD_LOCAL.set("tom");
System.out.println("Thread :" + Thread.currentThread().getId() + " set user = tom ");
});
TimeUnit.SECONDS.sleep(1);
for (int i = 0; i < 10; ++i) {
executorService.execute(() -> {
String value = THREAD_LOCAL.get();
System.out.println("Thread :" + Thread.currentThread().getId() + ", User:" + value);
});
}
}
}
这次 增加了线程ID的打印,运行结果如下:
Thread :12 set user = tom
Thread :12, User:tom
Thread :12, User:tom
Thread :13, User:null
Thread :12, User:tom
Thread :12, User:tom
Thread :13, User:null
Thread :13, User:null
Thread :12, User:tom
Thread :12, User:tom
Thread :13, User:null
至此,我们发现12号线程都可以获取到先前设置的当前用户,而13号线程获取的都是为null,原因一目了然了: 我们在12号线程起初设置的当前用户,那么只能在12号线程里获取到,13号线程当然为null了。
3、代码问题修正
上述代码表明ThreadLocal内的只能获取当前线程存储的值,当本线程不再使用时务必要remove删除掉:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocal01Test {
public static final ExecutorService executorService = Executors.newFixedThreadPool(2);
public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
executorService.execute(() -> {
try {
THREAD_LOCAL.set("tom");
}finally {
THREAD_LOCAL.remove();
}
});
TimeUnit.SECONDS.sleep(1);
for (int i = 0; i < 10; ++i) {
executorService.execute(() -> {
String value = THREAD_LOCAL.get();
System.out.println("User:" + value);
});
}
}
}
4、 代码优化改进
为了保证能释放ThreadLocal
关联的实例,我们可以通过AutoCloseable
接口配合try (resource) {...}
结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的ThreadLocal
可以封装为一个UserContext
对象:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocal03Test {
public static final ExecutorService executorService = Executors.newFixedThreadPool(2);
private static 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();
}
}
public static void main(String[] args) throws InterruptedException {
executorService.execute(() -> {
try (UserContext userContext = new UserContext("tom")) {
String value = UserContext.currentUser();
System.out.println("[1] Thread :" + Thread.currentThread().getId() + ", User:" + value);
}
});
TimeUnit.SECONDS.sleep(1);
for (int i = 0; i < 10; ++i) {
executorService.execute(() -> {
try (UserContext userContext = new UserContext("Join")) {
String value = userContext.currentUser();
System.out.println("[2] Thread :" + Thread.currentThread().getId() + ", User:" + value);
}
});
}
}
}