ThreadLocal 是什么?
ThreadLocal 的 JavaDoc 上的描述是这样的...
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).
这个类提供了线程本地变量。这个变量和正常的变量不同。通过 get & set 方法,每个线程可以获取到自己独立的变量。这个变量实例通常是私有且静态的,可以存储与线程相关的信息,如员工 id、事务 id 等。
ThreadLocal 能解决什么问题?
线程并发问题。因为 ThreadLocal 解决了变量共享的问题。ThreadLocal 还有一个隐含的好处,那就是不需要将参数在方法中进行传递,可以直接从线程中获取。
举个栗子:
- SimpleDateFormat 是线程不安全的,所以很多项目中在使用 SimpleDateFormat 对象的时候都是将其放入 ThreadLocal 中。
- Shiro 中的 Subject 就是采用 ThreadLocal 实现的~SecurityUtils.getSubject();
- Spring 中事务信息就是通过 ThreadLocal 进行传递的
@Test
public void testSimpleDateFormat() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < 10; ++i) {
new Thread(() -> {
try {
System.out.println(sdf.parse("2017-12-13 15:17:27"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
上述代码运行,就会大概率出现 java.lang.NumberFormatException
(如果运气好没出现,可以尝试将线程数量调大一点儿)
SimpleDateFormat 之所以是非线程安全的是因为其内部 Calendar 是线程不安全的。归根结底是因为 Calendar 存放日期数据的变量。
ThreadLocal 是怎么设计的?
ThreadLocal 的类结构图
ThreadLocal 类主要是用了一个内部 ThreadLocalMap,这个 map 中存储的键值对是 Entry<ThreadLocal,Object>,Entry 类中只有 value 成员变量,key 是 ThreadLocal 对象,Entry 继承了 WeakReference
。
为什么要使用弱引用呢,起初的设计估计是为了让 GC 自动去清理已经挂掉的线程的相关 value。但是这有个问题,我们待会儿分析。
ThreadLocal 本身并没有持有这个 Map 对象,而是让 Thread 对象持有 Map 对象,大家可以想想这个是为什么。
ThreadLocal & ThreadLocalMap & Thread & Entry 的关系
- Thread 只能拥有一个 ThreadLocalMap 对象。
- 一个 ThreadLocalMap 对象存储多个 Entry 对象。
- Entry 对象的 key 弱引用指向一个 ThreadLocal 对象。
- 一个 ThreadLocal 对象可以被多个线程共享。
- ThreadLocal 对象不持有 value 对象,value 由 Entry 对象持有。
ThreadLocal 的灵魂
- set() 如果不设置值,那么容易引起脏数据问题。(比如上次这个线程被线程池回收,但是没有调用 remove,下文我们会提到这个问题)
- get() 如果没有 get(),那么我们找不到使用 ThreadLocal 的意义...
- remove() remove 方法一个是保证,接下来使用的时候不会出现脏数据,另外就是保证弱引用的 Entry 的 value 能被 GC 回收,不然会出现内存泄漏...下面有代码演示
ThreadLocal 应该怎么用?
我们一般用 ThreadLocal 的时候都会在一个类中,声明一个静态对象,让类持有 ThreadLocal。
再调用其 set()、get()方法,来实现赋值和取值。具体可以看如下代码
@Test
public void testThreadLocal() throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(Task::new).start();
}
for (int i = 0; i < 2; i++) {
Thread.sleep(2000);
}
}
static class Task implements Runnable {
@Override
public void run() {
setName(Thread.currentThread().getName());
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
log.error(e.getMessage());
}
printName(Thread.currentThread().getName());
NameThreadLocal.remove();
}
/**
* threadLocal保存一下线程相关名称
*
* @param name
*/
static void setName(String name) {
NameThreadLocal.setName(name);
}
/**
* 打印一下 取出ThreadLocal的名称,看是否和当前线程一致
*
* @param threadName
*/
static void printName(String threadName) {
log.info(threadName + "======" + NameThreadLocal.getName());
}
}
/**
* 此类持有ThreadLocal静态对象
* 此处的Name可以是登录状态也可以是分布式的请求的traceId等等
*/
static class NameThreadLocal {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static void setName(String name) {
threadLocal.set(name);
}
static String getName() {
return threadLocal.get();
}
static void remove() {
threadLocal.remove();
}
}
// 结果如下
//Thread-3======Thread-3
//Thread-8======Thread-8
//Thread-5======Thread-5
//Thread-11======Thread-11
//Thread-7======Thread-7
//Thread-9======Thread-9
//Thread-12======Thread-12
//Thread-10======Thread-10
//Thread-6======Thread-6
//Thread-4======Thread-4
ThreadLocal 使用时有什么坑?
ThreadLocal 的 value 不能放共享变量
假设有一个共享变量 Object。
线程 A 设置 Object 的成员变量 property 为 x,放入了 ThreadLocal。
线程 B 设置 Object 的同一个成员变量 property 为 y,放入了 ThreadLocal。
假设两个线程同时执行,我们无法保证线程 B 再 get 的时候拿到的 property 的值是 x。
我们对上面的 Task 类代码进行改造,如下
static class Task implements Runnable {
// 一个共享变量
static StringBuilder sb = new StringBuilder("start");
static AtomicInteger errorNum = new AtomicInteger(0);
@Override
public void run() {
for (int i = 0; i < 50; i++) {
try {
TimeUnit.MILLISECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
sb.append(i);
}
setName(Thread.currentThread().getName() + sb.toString());
printName(Thread.currentThread().getName() + sb.toString());
}
/**
* threadLocal保存一下线程相关名称
*
* @param name
*/
static void setName(String name) {
NameThreadLocal.setName(name);
}
/**
* 打印一下 取出ThreadLocal的名称,看是否和当前线程一致
*
* @param threadName
*/
static void printName(String threadName) {
if (!threadName.equals(NameThreadLocal.getName())) {
errorNum.getAndIncrement();
log.error(errorNum.toString());
}
}
}
经过测试,errorNum 基本上都是大于 0。大家可以自己试一下~
ThreadLocal 遇上线程池
ThreadLocal 遇上线程池的问题,就是容易发生内存泄漏。
我们还是对 Task 类进行改造,再增加一个测试方法。
static class Task implements Runnable {
// 一个共享变量
static AtomicInteger errorNum = new AtomicInteger(0);
@Override
public void run() {
// 我们将前两个请求设置名称,后面的不设置
if (errorNum.getAndIncrement() > 2) {
setName(Thread.currentThread().getName());
}
printName(Thread.currentThread().getName());
}
/**
* threadLocal保存一下线程相关名称
*
* @param name
*/
static void setName(String name) {
NameThreadLocal.setName(name);
}
/**
* 打印一下 取出ThreadLocal的名称,看是否和当前线程一致
*
* @param threadName
*/
static void printName(String threadName) {
log.info(threadName + "=======" + NameThreadLocal.getName());
}
}
// 增加一个测试方法
@Test
public void testMemoryLeak() {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.submit(new Task());
}
}
// 结果如下
//pool-1-thread-2=======null
//pool-1-thread-3=======null
//pool-1-thread-1=======pool-1-thread-1
//pool-1-thread-2=======pool-1-thread-2
//pool-1-thread-1=======pool-1-thread-1
//pool-1-thread-3=======pool-1-thread-3
//pool-1-thread-2=======pool-1-thread-2
//pool-1-thread-1=======pool-1-thread-1
//pool-1-thread-3=======pool-1-thread-3
通过上面的结果我们看到,即使一个线程执行完成了任务,在没有主动清空 ThreadLocal 的时候,再用之前线程池中的线程还会带有之前的 ThreadLocal 对应的 value.因为线程并没有被回收,ThreadLocal 的键值对并不会被清空,所以也就解答了上文我们提到的问题。这种问题一旦出现,会比较隐蔽。
解决办法就是我们在使用完成之后,需要主动进行释放
ThreadLocal.remove()
Thread 创建了子线程,ThreadLocal 怎么办?
我们可以看到 Thread 类中有一个成员变量是用来处理此情况的。
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
我们可以直接使用 ThreadLocal 的子类**InheritableThreadLocal。但是继承等操作在线程池中的话,可能会因为动态的创建线程而变得非常混乱。所以不是很建议在线程池用 InheritableThreadLocal。**在看代码的时候,可以看一下此方法的调用。
java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)
总结
ThreadLocal 虽然有一些问题,但是我们不能因噎废食,否认其优秀。希望大家在使用的时候,注意上面的坑。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于