1. ThreadLocal介绍
1.1 ThreadLocal是什么?
定义:提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)。
1.2 ThreadLocal使用场景
典型场景1:每个线程需要一个独享的对象(通常是工具类,典工具类型需要使用的类有SimpleDateFormat
和Random
)
典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
我们来看看场景一的例子:
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) { // 新建了1000个SimpleDateFormat对象
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public static String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
这里1000个线程即便使用了线程池,但是每个线程都会在执行过程中创建一个SimpleDateFormat
对象,这比较耗费内存资源。
改进一:将SimpleDateFormat
提出来用static
修饰,这样每个线程都可以公用一个SimpleDateFormat
对象,减少内存消耗,但是这样会打印出相同的时间,所有线程都在争夺这个资源,我们需要一个锁去控制,避免出现线程安全问题。
改进二:在改进一的基础上添加锁控制,代码如下:
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public static String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalTest.class) {
s = dateFormat.format(date);
}
return s;
}
}
这虽然能够满足要求,但是在高并发场景下,所有线程需要一个个的去获取锁,需要排队等待,这显然性能损耗太大。
改进三:使用ThreadLocal
(不仅线程安全,而且也没有synchronized
带来的性能问题,每个线程内有自己独享的SimpleDateFormat
对象)
// 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
public class ThreadLocalTest {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public static String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); // 拿到initialValue返回对象
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
// lambda表达式写法,和上面写法效果完全一样
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
像这种需要每个线程内独享的对象,一般使用场景是工具类中。后面再讲解原理,讲讲每个线程为什么都有独享的对象,这里先看用法。
我们来看看场景二的例子
需求:当前用户信息需要被线程内所有方法共享
当一个请求进来了,一个线程负责处理该请求,该请求会依次调用service-1()
, service-2()
, service-3()
, service-4()
,同时,每个service()
都需要获得调用方用户user
的信息,也就是需要拿到user
对象。
一个比较繁琐的解决方案是把user
作为参数层层传递,从service-1()
传到service-2()
,再从service-2()
传到service-3()
,以此类推,但是这样做会导致代码冗余且不易维护。
在此基础上可以演进,使用UserMap
,就是每个用户的信息都存在一个Map
中,当多线程同时工作时,我们需要保证线程安全,可以用synchronized
也可以用ConcurrentHashMap
,但这两者无论用什么,都会对性能有所影响。
有没有更好的方法呢?ThreadLocal
就来了
public class ThreadLocalTest {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("张三");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用户名:" + );
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + );
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>(); // 对比上一个例子,这里没有重写initialValue方法
}
class User {
String name;
public User(String name) {
= name;
}
}
运行结果:
这样,不管哪个Service
都能拿到User
对象,能获取User
对象内的所有信息。并且假如有多个请求,一个张三,一个李四,因为他们并没有直接共享User
对象,所以他们之间不会有线程安全问题。
使用ThreadLocal
后无需synchronized
,可以在不影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程对应的用户信息的目的。
后面从源码再说说为什么这里ThreadLocal
不会有线程安全问题。
2. ThreadLocal的作用和好处
2.1 ThreadLocal的两个作用
- 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
- 在任何方法中都可以轻松获取到该对象
2.2 ThreadLocal两种用法
根据共享对象的生成时机不同,选择initialValue
或set
来保存对象
- 不受外界传参影响的时候,可以选择重写
initialValue()
方法来初始化保存对象,会在ThreadLocal
第一次调用get()
方法的时候初始化对象,对象的初始化时机可以由我们控制,比如上面第一个例子工具类。 - 如果需要保存到
ThreadLocal
里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set
直接放到我们的ThreadLocal
中去,以便后续使用,对应代码就是上面第二个例子。
2.3 使用ThreadLocal带来的四个好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 更高效地利用内存节省开销,上面例子中,相比于成千上万个任务,每个任务都新建一个
SimpleDateFormat
,显然用ThreadLocal
可以节省内存和开销。 - 免去传参的繁琐,不需要每次都传同样的参数,
ThreadLocal
使得代码耦合度更低,更优雅
3. ThreadLocal主要方法介绍
主要是initialValue
、set
、get
、remove
这几个方法,关于源码分析,将在第4节介绍
initialValue
方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get
的时候,才会触发。- 当线程第一次使用
get
方法访问变量时,将调用initialValue
方法,除非线程先前调用了set
方法,在这种情况下,不会为线程调用本initialValue
方法。 - 通常,每个线程最多调用一次
initialValue()
方法,但如果已经调用了一次remove()
后,再调用get()
,则可以再次调用initialValue()
,相当于第一次调用get()
。 - 如果不重写
initialValue()
方法,这个方法会返回null
。一般使用匿名内部类的方法来重写initialValue()
方法,以便在后续使用中可以初始化副本对象。
4. ThreadLocal原理源码分析
4.1 Thread、ThreadLocal、ThreadLocalMap三者的关系
从图中可以看出,每个Thread
对象都有一个ThreadLocalMap
,每个ThreadLocalMap
可以存储多个ThreadLocal
4.2 get方法
public T get() {
Thread t = Thread.currentThread();
// 如果之前调用过set方法,那么这里getMap就不为null
ThreadLocalMap map = getMap(t); // getMap就是看看当前线程有没有创建ThreadLocalMap集合,如果没有,这个集合就是为null
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// 调用过set会从这里return
return result;
}
}
// 如果当前线程还没有创建ThreadLocalMap,执行setInitialValue方法
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue(); // 调用你重写的initialValue方法,获取返回值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
/*
只有第一次使用get方法才调用initialValue方法的原因,第一次创建ThreadLocalMap
第二次及以后,getMap发现ThreadLocalMap不是null,走不到这个方法来了。
set存的key是什么?this是当前ThreadLocal对象!
*/
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
说明:get
方法是先取出当前线程的ThreadLocalMap
,然后调用map.getEntry
方法,把本ThreadLocal
的引用作为参数传入,取出map
中属于本ThreadLocal
的value
注意:这个map
以及map
中的key
和value
都是保存在线程中ThreadLocalMap
的,而不是保存在ThreadLocal
中
getMap
方法:获取到当前线程内的ThreadLocalMap
对象
每个线程内都有ThreadLocalMap
对象,名为threadLocals
,初始值为null
4.3 set方法
因为set
方法与setInitialValue
方法很类似,这里分析一下set
方法
// 把当前线程需要全局共享的value传入
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// map对象为空就创建,不为空就覆盖
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
4.4 initialValue方法
这个方法没有默认实现,如果要用initialValue
方法,需要自己实现,通常使用匿名内部类的方式实现(可以回顾上面代码)
4.5 remove方法
// 删除对应这个线程的值
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 移除这个ThreadLocal对应的值
m.remove(this);
}
4.6 ThreadLocalMap类
ThreadLocalMap
类,也就是Thread.threadLocals
// 此行声明在Thread类中,创建ThreadLocalMap就是对Thread类的这个成员变量赋值
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap
类是每个线程Thread
类里面的变量,但ThreadLocalMap
这个静态内部类定义在ThreadLocal
类中,其中发现这一行代码
private Entry[] table;
里面最重要的是一个键值对数组Entry[] table
,可以认为是一个map
,键值对:
- 键:这个
ThreadLocal
- 值:实际需要的成员变量,比如
User
或者SimpleDateFormat
对象
这个思路和HashMap
一样,那么我们可以把它想象成HashMap
来分析,但是实现上略有不同。
比如处理冲突方式不同,HashMap
采用链地址法,而ThreadLocalMap
采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链
通过源码分析可以看出,setInitialValue
和直接set
最后都是利用map.set()
方法来设置值,最后都会对应到ThreadLocalMap
的一个Entry
5. ThreadLocal需要注意的点
5.1 ThreadLocal内存泄漏问题
什么是内存泄漏?
某个对象不再有用,但是占用的内存却不能被回收
ThreadLocalMap
中的Entry继承自WeakReference
,是弱引用- 弱引用:通过
WeakReference
类实现的,在GC
的时候,不管内存空间足不足都会回收这个对象,适用于内存敏感的缓存,ThreadLocal
中的key
就用到了弱引用,有利于内存回收。 - 强引用:我们平日里面的用到的
new
了一个对象就是强引用,例如Object obj = new Object();
当JVM
的内存空间不足时,宁愿抛出OutOfMemoryError
使得程序异常终止也不愿意回收具有强引用的存活着的对象。
ThreadLocal
可能出现Value
泄漏!
ThreadLocalMap
的每个 Entry
都是一个对key
的弱引用,同时,每个 Entry
都包含了一个对value
的强引用,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key值给WeakReference处理
value = v; // value直接用变量保存,是强引用
}
}
正常情况下,当线程终止,保存在ThreadLocalMap
里的value
会被垃圾回收,因为没有任何强引用了。但如果线程不终止(比如线程需要保持很久),那么key
对应的value
就不能被回收,因为有以下的调用链:
Thread
---->ThreadLocalMap
---->Entry
(key
为null
,弱引用被回收)---->value
因为value
和Thread
之间还存在这个强引用链路,所以导致value
无法回收,就可能会出现OOM
JDK
已经考虑到了这个问题,所以在set
, remove
, rehash
方法中会扫描key
为null
的Entry
,并把对应的value
设置为null
,这样value
对象就可以被回收
比如rehash
里面调用resize
private void resize() {
......省略代码
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
}
......
如果key
回收了,那么value
也设置为null
,断开强引用链路,便于垃圾回收。
但是如果一个ThreadLocal
不被使用,那么实际上set
, remove
, rehash
方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value
的内存泄漏
5.2 ThreadLocal如何避免内存泄漏?
及时调用remove
方法,就会删除对应的Entry
对象,可以避免内存remove
泄漏,所以使用完ThreadLocal
之后,应该调用remove
方法。
比如拦截器获取到用户信息,用户信息存在ThreadLocalMap
中,线程请求结束之前拦住它,并用remove
清除User
对象,这样就能稳妥的保证不会内存泄漏。
5.3 共享对象问题
如果在每个线程中ThreadLocal.set()
进去的东西本来就是多线程共享的同一个对象,比如static
对象,那么多个线程的ThreadLocal.get()
取得的还是这个共享对象本身,还是有并发访问问题。
5.4 不要强行使用ThreadLocal
如果可以不使用ThreadLocal
就能解决问题,那么不要强行使用,在任务数很少的时候,可以通过在局部变量中新建对象解决。
5.5 优先使用框架的支持,而不是自己创造
在Spring
中,如果可以使用RequestContextHolder
,那么就不需要自己维护ThreadLocal
,因为自己可能会忘记调用remove()
方法等,造成内存泄漏。
6. 在Spring中实例中哪里用到了ThreadLocal?
DateTimeContextHolder
类,应用了ThreadLocal
ThreadLocal
的典型应用场景:每次HTTP
请求都对应一个线程,线程之间相互隔离- 看
RequestContextHolder
,也是用到了ThreadLocal
,看NamedThreadLocal
源码,再看getRequestAttributes
的调用