聊一聊ThreadLocal的那些事
1.ThreadLocal的简介
ThreadLocal,是一个提供线程本地变量的类。这些变量不同于普通的变量,每个线程都会初始化一个完全独立的变量副本。ThreadLocal在使用的时候,通常使用 private static来修饰,用以关联一个线程的状态信息。每个线程都能通过get和set方法来获取和设置该线程自己得变量实例,从而实现变量在不同线程之间的隔离,在同一线程共享。
2.ThreadLocal的简单实例
public class Test {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
threadLocal.set("Thread-1");
System.out.println("t1:" + threadLocal.get());
});
Thread t2 = new Thread(() -> {
threadLocal.set("Thread-2");
System.out.println("t2:" + threadLocal.get());
});
t1.start();
t2.start();
}
}
运行结果如下:
t1:Thread-1
t2:Thread-2
3.ThreadLocal的原理
3.1 ThreadLocal.set()方法的原理
我们先看它的源码:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
set赋值的时候,先获取当线程,然后获取当前线程的属性ThreadLocalMap,如果ThreadLocalMap存在,就直接以threadLocal为key设置变量值;如果ThreadLocalMap不存在,通过传递当前线程的方法createMap,创建一个ThreadLocalMap作为当前线程Thread的属性。这里面我们最需要注意的就是:
ThreadLocalMap是Thread的一个属性。这一点很重要。
通过上面的源码,我们可以看到,ThreadLocalMap这个类起到了非常重要的作用,下面我们看下ThreadLocalMap是什么?
ThreadLocalMap是一个定制化的Hash Map,仅仅用来维护线程本地值。
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;
}
}
}
通过上面的源码我们看出,ThreadLocalMap是ThreadLocal的一个内部静态类,使用Entry来保存数据。在Entry保存数据时,使用ThreadLocal来作为key,使用我们设置的value作为value.值得注意的是,Entry使用的key为ThrealLocal 的一个弱引用,这样 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的。到这里我们似乎就霍然开朗了很多,其实所谓的ThreadLocal,它最主要的作用我认为有两个:
- 作为ThreadLocalMap的保存数据的key,ThreadLocalMap又是Thread的一个内部的属性(ThreadLocalMap是每个线程都会在调用时(set)进行初始化,即在每个线程中,ThreadLocalMap都会重新实例化后,然后再作为当前线程的属性),这样就实现了是所谓的副本拷贝。
- 提供各种调用的api(如set、get和remove)和各种内部的类及方法。
所以**,ThreadLocal中真正存储数据的是ThreadLocalMap**。
3.2 ThreadLocalMap为什么使用ThreadLocal的弱引用作为key
我们还是看上满Test中的代码,t1、t2两个线程中都引用了ThreadLocal,这是一个强引用,t1又引用ThreadLocalMap,ThreadLocalMap又弱引用了ThreadLocal,如下图所示:
如果我在t1中加一句:threadLocal == null,那么此时的threadLocal就可以回收了,因为它现在就只有两个弱引用了,而弱引用是在垃圾回收时,是一定会回收的;假如我把弱引用改为强引用,那么此时threadLocal是不可以被回收的,必须要等线程结束之后threadLocal 才会被回收,当线程数过多,线程持续时间较长是,就会有内存溢出的风险。
3.3 ThreadLocal.get()方法的原理
我们先看ThreadLocal的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;
}
如果存在,就返回ThreadLocalMap中保存的值,如果不存在,就返回初始值null.
3.4 ThreadLocal.remove()方法的原理
顾名思义,就是清除当前线程中的ThreadLocalMap保存的值。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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;
}
}
}
4. ThreadLocal的使用场景
- ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。
我们知道,SimpleDateFormat是线程不安全的,在多线程使用的时候,需要给每个线程都创建一个SimpleDateFormat实例,如果线程过多,那么就会创建过多的SimpleDateFormat对象。如果我们使用ThreadLocal,那么就只需要创建线程池中所需的数量的SimpleDateFormat实例就可以了。
public class ThreadLocalSimpleDateFormat {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
threadPool.execute(() -> {
Date date = new Date();
String data = dateFormatThreadLocal.get().format(date);
System.out.println(data);
});
}
}
}
- ThreadLocal用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念。
public class ThreadLocalUser {
public static ThreadLocal<Student> holder = new ThreadLocal<>();
public static void main(String[] args) {
Student student = new Student("zhangsan");
holder.set(student);
new StudentService().getStudentName();
holder.remove();
}
}
class StudentService {
public void getStudentName() {
System.out.println(ThreadLocalUser.holder.get().getName());
}
}
5. ThreadLocal在使用时候的一些坑
5.1 忘记使用remove,造成数据的错乱
在springboot中,我写了这么一个程序:
@RestController
@RequestMapping
public class UserContropller {
private static final ThreadLocal<Integer> currentUserId = ThreadLocal.withInitial(() -> null);
@RequestMapping("/user/{userId}")
public Map getUserById(@PathVariable(value = "userId") Integer userId) {
Map map = new HashMap<>();
String before = Thread.currentThread().getName() + ":" + currentUserId.get();
currentUserId.set(userId);
String after = Thread.currentThread().getName() + ":" + currentUserId.get();
map.put("before", before);
map.put("after", after);
return map;
}
}
然后设置tomcat的线程池的线程数为1:server.tomcat.threads.max=1
第一次访问:http://localhost:8080/user/1
结果:
{"before": "[http-nio-8080-exec-1:null](http-nio-8080-exec-1:null)",
"after": "[http-nio-8080-exec-1:1](http-nio-8080-exec-1:1)"
}
显然结果是符合预期的。
第二次访问:http://localhost:8080/user/2
结果:
{
"before": "http-nio-8080-exec-1:1",
"after": "http-nio-8080-exec-1:2"
}
我们看到,结果不符合预期,第一次访问的结果仍然存在。
原因分析:因为我们这个程序是运行在tomcat这种web服务器下,这本身就是运行在一个多线程的环境下,因为线程的创建比较昂贵,所以web服务器往往会使用线程池来处理请求,那么线程就会重用,此时,使用ThreadLocal来存放数据时,就需要显式地区清空设置的数据。
代码修正:
public class UserContropller {
private static final ThreadLocal<Integer> currentUserId = ThreadLocal.withInitial(() -> null);
@RequestMapping("/user/{userId}")
public Map getUserById(@PathVariable(value = "userId") Integer userId) {
Map map = new HashMap<>();
String before = Thread.currentThread().getName() + ":" + currentUserId.get();
currentUserId.set(userId);
try {
String after = Thread.currentThread().getName() + ":" + currentUserId.get();
map.put("before", before);
map.put("after", after);
return map;
} finally {
currentUserId.remove();
}
}
}
5.2 忘记使用remove,造成内存泄露
我们知道,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部的强引用,那么ThreadLocal就会被垃圾回收掉,那么此时ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。假如线程非常多,并且每个线程都执行时间比较长,那么ThreadLocalMap中key为null的Entry中value就会一直存在一条强引用:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
这就有造成内存泄露的风险。