ThreadLocal又叫线程变量,ThreadLocal为变量在每个线程都创建了一个副本,该副本只属于当前线程,可以在线程内任何地方拿到这个变量,每个线程中变量的副本值都可以不同,起到了线程隔离的作用。使用时ThreadLocal变量一般被声明为private static
。
1、ThreadLocal使用场景
当遇到以下场景时,可以考虑使用ThreadLocal:
- 一个线程里的很多方法都要传入相同的参数时,可以把参数写到ThreadLocal里,方法内部用到的时候直接调用ThreadLocal的get方法即可,不需要通过方法入参的方式传入,即全局存储变量信息;
- 需要实现线程隔离,即每个线程里的变量都不同,比如一个线程处理一个请求,线程里需要用到这次请求的发起时间、结束时间和requestId,且这些值在每个线程里都是不同的;
- 线程安全问题,比如数据库连接会用单例模式生成一个connect,如果一个线程在connect打开后做任务的同时,另一个线程做完了任务关闭connect就会出问题,此时用synchronized锁是一种解决方案,但是时间效率会低,即synchronized是用时间换空间;将connect存入到ThreadLocal变量,在每个线程里都保留一个副本是另一个解决方案,但是会占用空间,即空间换时间。
当遇到以下场景时,要慎用ThreadLocal:
- 线程池里的工作线程由于执行完任务后不会被销毁,仍然会保留上一个业务的线程变量,因此线程池里的工作线程每次执行完任务后,都应手动remove一下线程变量,防止下一个任务里用的是上一个任务的线程变量值;
- 异步编程且可能存在在两个异步线程间传递线程变量的场景,比如两个异步线程,一个是前端接收请求后给后端,另一个是后端自己处理请求返回值,一次请求对应一个requestId,且这个requestId在两个线程内都要存在并且保持一致,这时在前端的线程里用UUID生成一个requestId后很难传递给负责后端的线程。
2、ThreadLocal使用方法
2.1 ThreadLocal类的方法
首先看一下ThreadLocal中的几个比较重要的方法:
get方法用来获取ThreadLocal变量的值,如下:public T get() {}
public void set(T value) {}
public void remove() {}
protected T initialValue() {}
public static <S> ThreadLocal<S> withInitial() {}
set方法用来给ThreadLocal变量设值,如下:ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
String str = stringThreadLocal.get();
remove方法用来清除ThreadLocal变量里的值,尤其是用线程池创建的工作线程,由于工作线程执行完任务后不会被线程池销毁,且线程变量是在整个线程生命周期内存在的,上一个任务对应的线程变量没有被销毁可能会影响下一个任务的逻辑,因此在线程池中执行完任务后要手动remove线程变量,使用方式如下:ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("Jerry");
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.remove();
initialValue
方法和withInitial
方法是用来给线程变量赋初始值的,因为有这样的场景:线程变量还没有set就被get了,此时initialValue
方法会默认返null
,如果不想返回null
而是想返回指定值时,可以在new
线程变量时覆写initialValue
方法,或者用lambda
表达式,这里推荐用lambda
表达式,如下:ThreadLocal<String> stringThreadLocal = new ThreadLocal<>().withInitial(() -> "Jerry");
2.2 ThreadLocal使用Demo
2.2.1 线程隔离
假设线程池的每个工作线程处理一个请求接入,每个请求包含这次请求唯一对应的UUID:requestId,请求的发起时间:startTime和结束时间:endTime,使用newSingleThreadExecutor
创建一个仅有一个工作线程的线程池,使请求串行执行,方便区分startTime和endTime。构造一个RequestInfo
对象,requestId、startTime和endTime分别是对象的ThreadLocal属性,显然一次请求的requestId、startTime和endTime都应该是这次请求独有的。
RequestInfo: ```java package Concurrent.ThreadLocal;
import java.text.SimpleDateFormat; import java.util.Date; import java.util.UUID;
public class RequestInfo {
private static ThreadLocal
private final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public void setRequestId()
{
requestId.set(UUID.randomUUID().toString());
}
public void setStartTime()
{
startTime.set(df.format(new Date()));
}
public void setEndTime()
{
endTime.set(df.format(new Date()));
}
public void remove()
{
requestId.remove();
startTime.remove();
endTime.remove();
}
public void printRequestInfo()
{
System.out.print("requestId: " + requestId.get() + ", ");
System.out.print("start_time: " + startTime.get() + ", ");
System.out.print("end_time: " + endTime.get());
}
}
三个线程变量初始化时用withInitial将初始值设置为字符串"Empty",`RequestInfo`里将线程变量的get、set和remove都包了一层,方便外部调用。
Main:
```java
package Concurrent.ThreadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
int tastNum = 3;
final RequestInfo requestInfo = new RequestInfo();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < tastNum; ++i) {
executorService.execute(() ->
{
requestInfo.setRequestId();
requestInfo.setStartTime();
// 模仿每个线程执行业务需要2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
requestInfo.setEndTime();
requestInfo.printRequestInfo();
System.out.println();
// 每个任务间隔3s
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动清除线程池工作线程里的线程变量,防止该工作线程里的线程变量影响下一个业务逻辑
requestInfo.remove();
});
}
executorService.shutdown();
}
}
运行结果如下:
requestId: 56fc06a4-d407-4daa-9e78-5b2698c759d4, start_time: 2020-06-03 16:33:13, end_time: 2020-06-03 16:33:15
requestId: 95189dd7-d19c-4053-9108-3abe5fb0d4b5, start_time: 2020-06-03 16:33:18, end_time: 2020-06-03 16:33:20
requestId: 0a682cac-9929-40f1-9f88-50ba171dd91f, start_time: 2020-06-03 16:33:23, end_time: 2020-06-03 16:33:25
线程池起了3个线程任务串行执行,任务执行时间为2s,任务执行间隔为3s,每个请求都对应一个不同的requestId,每个请求的开始时间和结束时间都独立对应。
2.2.2 省略传参
User类:
@Data
public class User {
private String name;
private String id;
private int age;
}
TestClass类:
public class TestClass {
private static ThreadLocal<User> userThreadLocal;
static {
User user = new User();
user.setAge(28);
user.setId("1");
user.setName("Jerry");
userThreadLocal.set(user);
}
public void method1() {
User user = userThreadLocal.get();
...
}
public void method2() {
User user = userThreadLocal.get();
...
}
public void method3() {
User user = userThreadLocal.get();
...
}
}
使用了ThreadLocal变量,类中的方法直接在方法实现里通过ThreadLocal的get方法就能获取User对象,如果不使用ThreadLocal变量,采用方法传参的方式传入User,则每个方法的入参都要有一个User对象,会使方法定义时的入参较多,代码冗余。
2.2.3 线程间共享TheadLocal
ThreadLocal的一大用途就是线程隔离,但如果我想让线程间能共享ThreadLocal变量该如何实现呢?虽然这么做有悖于ThreadLocal变量的初衷,但作为了解可以知晓一下。使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
举例如下:
public class Main {
public static void main(String[] args) {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("帅得一匹");
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println( "张三帅么 =" + threadLocal.get());
}
};
t.start();
}
}
结果:子线程中获取到了在主线程中定义的InheritableThreadLocal实例的值,如下图:
大致分析一下InheritableThreadLocal实例的value是如何在线程间传递的:首先InheritableThreadLocal类是ThreadLocal类的子类,在Thread类的源码中的init方法里,部分源码如下:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
...
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
上面代码是说:如果线程的inheritThreadLocals变量不为空,并且父线程的inheritThreadLocals也存在,那么就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。
3、ThreadLocal && ThreadLocalMap源码分析
ThreadLocal中的get/set方法,底层都是ThreadLocalMap中的private方法实现的,ThreadLocalMap并不对外提供get/set方法。ThreadLocalMap是ThreadLocal的一个内部实现类,在Thread类中有ThreadLocalMap的引用。
3.1 TheadLocalMap
看ThreadLocal的get和set源码会发现底层都是操作的这个TheadLocalMap对象,因此我们首先分析ThreadLocalMap的源码。这里先抛出一个结论:线程变量存储的值并不是在ThreadLocal中存的,而是在每个线程的ThreadLocalMap中以k-v存储,key代表ThreadLocal实例,value代表线程变量存储的值。
Thread类中关于ThreadLocalMap的源码如下:
public class Thread implements Runnable {
……
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
……
}
可以看到这个ThreadLocalMap是每个线程Thread单独维护的,这也是ThreadLocal可以实现线程隔离的原因。
再看一下ThreadLocalMap类的具体实现,它是ThreadLocal类的一个内部类:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
private Entry[] table;
...
}
注意:
- ThreadLocalMap虽然名字里有个Map,但是该类并没有实现Map接口;
- 底层是一个Entry内部类的数组table;
- Entry类继承了弱引用WeakReference类,与Map中的Entry一样也是一个k-v结构,key是一个ThreadLocal泛型类,value是线程变量真正存储的值。
3.2 ThreadLocal类的get方法
public T get() { Thread t = Thread.currentThread(); // 获取Thread类中的ThreadLocalMap实例threadLocals ThreadLocalMap map = getMap(t); if (map != null) { // 从ThreadLocalMap中获取内部类Entry的实例,getEntry方法的入参是ThreadLocal实例, // 底层是根据key的hash值计算出在table数组中的下标然后取对应Entry元素 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // Entry中的value即ThreadLocal中存储的值 T result = (T)e.value; return result; } } // 如果map不存在,需要返回一个默认值,设置默认值需要实现initialValue方法,该方法是一个protected方法 return setInitialValue(); }
3.3 ThreadLocalMap类的getEntry方法
ThreadLocal类的get方法底层是基于ThreadLocalMap类的getEntry方法和getEntryAfterMiss方法实现的,这里分析一下这两个方法的源码: ```java private Entry getEntry(ThreadLocal<?> key) { // 根据key通过hash算法计算得到的值与长度取模,得到在table数组中的下标,注意threadLocalHashCode方法不是Object类里的获取hashcode方法 int i = key.threadLocalHashCode & (table.length - 1); // 根据计算出来的下标从底层table数组中取出Entry实例 Entry e = table[i]; // 如果能从底层的table数组中获取到,则直接返回 if (e != null && e.get() == key)
elsereturn e;
}// 当没有从底层table数组中获取到Entry实例时的处理方法 return getEntryAfterMiss(key, i, e);
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length;
// 通过while循环在底层table数组中遍历,寻找Entry
while (e != null) {
ThreadLocal<?> k = e.get();
// 找到了就返回
if (k == key)
return e;
// 有些博客中提到针对ThreadLocal的内存泄漏问题,在ThreadLocalMap的set和get方法中会及时清理key为null的Entry,这里就是在做这个事情
if (k == null)
expungeStaleEntry(i);
else
// 获取下一个下标,推动while循环进行下去
i = nextIndex(i, len);
e = tab[i];
}
// 都没找到那就没辙了,返回null
return null;
}
<a name="NpQSF"></a>
## 3.4 ThreadLocal类的set方法
```java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 核心还是调用的ThreadLocalMap类的set方法
map.set(this, value);
else
createMap(t, value);
}
3.5 ThreadLocalMap类的set方法
ThreadLocal类的set方法底层是基于ThreadLocalMap类的set方法实现的,这里分析一下ThreadLocalMap类的set方法源码:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据key通过hash算法计算得到的值与长度取模,得到在table数组中的下标,注意threadLocalHashCode方法不是Object类里的获取hashcode方法
int i = key.threadLocalHashCode & (len-1);
// 这里是通过for循环遍历table数组,set过程也是在for循环内做的,注意这里包含了ThreadLocalMap解决哈希冲突的开放地址法
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// get方法是WeakReference类的父类Reference中的方法,目的是获取ThreadLocal实例
ThreadLocal<?> k = e.get();
// 如果目标key就是当前for循环遍历中Entry的k,则将目标value直接给底层Entry的value
if (k == key) {
e.value = value;
return;
}
// 有些博客中提到针对ThreadLocal的内存泄漏问题,在ThreadLocalMap的set和get方法中会及时清理key为null的Entry,这里就是在做这个事情
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果for循环结束后还没有找到对应的Entry,就基于目标key和value在底层重新new一个Entry对象,并根据计算得到的下标赋值给table数组,开放地址法解决哈希冲突,此时会在空的散列地址上新建一个entry
// 注意走到这里for循环已经结束了,看代码别看岔了
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从上面源码可以看出,ThreadLocalMap中set一个k-v还是很麻烦的,要先进行for循环,for循环如果没匹配到,才会基于k-v在底层新建一个Entry。因此当一个线程中的ThreadLocal实例很多时,再往这个线程里新增ThreadLocal实例可能存在效率问题,因为要先走一遍for循环。
3.6 ThreadLocalMap如何解决哈希冲突?
在HashMap中是通过拉链法解决的哈希冲突的,ThreadLocalMap虽然没有实现Map接口,但同样会有”Map”这种结构都会有的哈希冲突问题,那ThreadLocalMap是如何解决哈希冲突的呢?
ThreadLocalMap使用开放地址法寻址,所谓的开放地址法寻址,是指一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。在ThreadLocalMap的set方法中,通过 hashCode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过在table数组中不断的往后寻找一位,直到找到索引位置为空的地方,把当前 ThreadLocal 作为 key 放进去。
为什么ThreadLocalMap采用开放地址法寻址解决哈希冲突?
- Threadlocal对象不多,对象不多的话使用开放寻址法效率也不低(线性遍历);
- Thread local不多的话,哈希冲突的概率也不高,使用开放寻址实现更简单。
3.7 ThreadLocal类的remove方法
由于ThreadLocal有内存泄漏的风险,因此每次使用完ThreadLocal时,最好是手动调用remove方法,将ThreadLocalMap中的弱引用key置为null,尤其是在线程池中的工作线程中更应调用remove方法。有关内存泄漏的详细原因在第4小节中介绍。public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 底层还是调用的ThreadLocalMap的remove方法 m.remove(this); }
3.8 ThreadLocalMap类的remove方法
ThreadLocal类的remove方法底层是基于ThreadLocalMap类的remove方法实现的,这里分析一下ThreadLocalMap类的remove方法源码:private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; // 根据哈希算法取模,确认在table数组中的下标 int i = key.threadLocalHashCode & (len-1); // for循环遍历table数组 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { // 这里就是remove方法的核心,底层是Reference类的clear方法,实现就一行:this.referent = null; // 即将ThreadLocalMap中的key置为null,而真正的清理动作并不在这,而是在expungeStaleEntry方法 e.clear(); expungeStaleEntry(i); return; } } }
4、ThreadLocal内存泄漏
正因为ThreadLocal使有内存泄漏的风险,有些公司开发规范上已经明令禁止使用ThreadLocal了,本节分析一下ThreadLocal为什么会有内存泄漏的原因。4.1 为什么存在内存泄漏的问题?
首先要清楚Java的四种引用方式:- 强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象 ;
- 软引用:在使用软引用时,如果内存的空间足够,软引用指向的对象就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用指向的对象才会被垃圾回收器回收;
- 弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用指向的对象回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象 。
- 虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。
ThreadLocalMap里的key是ThreadLocal变量,且ThreadLocal作为map的key是弱引用,弱引用意味着系统GC
的时候弱引用指向的堆中的ThreadLocal对象会被回收,如图所示,图摘自链接2:
图中左侧Stack里存的是指向堆中对象的引用,右侧Heap则是堆空间,存的是真正的对象,垃圾回收也是作用在堆区。虚线代表ThreadLocalMap的key是弱引用,指向堆中的ThreadLocal变量,当系统gc时,如果堆中的ThreadLocal对象在Stack中没有强引用指向它(即引用值为null),便会被系统回收,此时ThreadLocalMap会出现key为null的Entry
,如果此时线程不被销毁的话(线程中的ThreadLocalMap的强引用就会一直存在),这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
,value引用指向的ThreadLocal存储的对象就永远无法被回收,造成内存泄漏。
其实ThreadLocal内存泄漏的原因并不全是因为ThreadLocalMap的key是弱引用,根因是线程一直不被销毁的话,ThreadLocalMap的value就一直存在强引用链,value指向的对象在堆中无法被垃圾回收,从而造成内存泄漏。
4.2 ThreadLocal为什么被设计为弱引用?
那既然ThreadLocal存在内存泄漏的风险,为什么还要把ThreadLocal设计成弱引用?设计成强引用是不是就没有内存泄漏的问题了?我们知道ThreadLocal存储的值实际是存储在ThreadLocalMap中的,而ThreadLocalMap本身没有对外提供set/get方法,一旦我们使用完毕将ThreadLocal对象的引用置为null时,ThreadLocalMap里存储的k-v依然存在,并没有被remove掉。Entry中的key设计成弱引用的目的就是为了让虚拟机栈中的ThreadLocal引用置为null时,自动地将ThreadLocalMap中对应的Entry指向的对象回收,在ThreadLocal引用置为null,ThreadLocalMap中对应的Entry指向的对象被回收之前,确实存在内存泄漏。但是我们不用担心,就算我们不调用ThreadLocalMap中的remove方法,ThreadLocalMap中的set,get和扩容时都会清理掉key为null的Entry,内存泄漏完全没必要过于担心。
参考
Java并发编程:深入剖析ThreadLocal
深入分析 ThreadLocal 内存泄漏问题
Java面试必问:ThreadLocal终极篇 淦!
谈谈ThreadLocal为什么被设计为弱引用
ThreadLocal为什么要使用弱引用和内存泄露问题