定义:提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)。
典型场景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拿到用户名:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>(); // 对比上一个例子,这里没有重写initialValue方法
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
运行结果:
这样,不管哪个Service
都能拿到User
对象,能获取User
对象内的所有信息。并且假如有多个请求,一个张三,一个李四,因为他们并没有直接共享User
对象,所以他们之间不会有线程安全问题。
使用ThreadLocal
后无需synchronized
,可以在不影响性能的情况下,也无需层层传递参数,就可以达到保存当前线程对应的用户信息的目的。
后面从源码再说说为什么这里ThreadLocal
不会有线程安全问题。
根据共享对象的生成时机不同,选择initialValue
或set
来保存对象
initialValue()
方法来初始化保存对象,会在ThreadLocal
第一次调用get()
方法的时候初始化对象,对象的初始化时机可以由我们控制,比如上面第一个例子工具类。
ThreadLocal
里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set
直接放到我们的ThreadLocal
中去,以便后续使用,对应代码就是上面第二个例子。
SimpleDateFormat
,显然用ThreadLocal
可以节省内存和开销。ThreadLocal
使得代码耦合度更低,更优雅主要是initialValue
、set
、get
、remove
这几个方法,关于源码分析,将在第4节介绍
initialValue
方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get
的时候,才会触发。
get
方法访问变量时,将调用initialValue
方法,除非线程先前调用了set
方法,在这种情况下,不会为线程调用本initialValue
方法。
initialValue()
方法,但如果已经调用了一次remove()
后,再调用get()
,则可以再次调用initialValue()
,相当于第一次调用get()
。
initialValue()
方法,这个方法会返回null
。一般使用匿名内部类的方法来重写initialValue()
方法,以便在后续使用中可以初始化副本对象。
从图中可以看出,每个Thread
对象都有一个ThreadLocalMap
,每个ThreadLocalMap
可以存储多个ThreadLocal
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
因为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);
}
这个方法没有默认实现,如果要用initialValue
方法,需要自己实现,通常使用匿名内部类的方式实现(可以回顾上面代码)
// 删除对应这个线程的值
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 移除这个ThreadLocal对应的值
m.remove(this);
}
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
什么是内存泄漏? 某个对象不再有用,但是占用的内存却不能被回收
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
的内存泄漏
及时调用remove
方法,就会删除对应的Entry
对象,可以避免内存remove
泄漏,所以使用完ThreadLocal
之后,应该调用remove
方法。
比如拦截器获取到用户信息,用户信息存在ThreadLocalMap
中,线程请求结束之前拦住它,并用remove
清除User
对象,这样就能稳妥的保证不会内存泄漏。
如果在每个线程中ThreadLocal.set()
进去的东西本来就是多线程共享的同一个对象,比如static
对象,那么多个线程的ThreadLocal.get()
取得的还是这个共享对象本身,还是有并发访问问题。
如果可以不使用ThreadLocal
就能解决问题,那么不要强行使用,在任务数很少的时候,可以通过在局部变量中新建对象解决。
在Spring
中,如果可以使用RequestContextHolder
,那么就不需要自己维护ThreadLocal
,因为自己可能会忘记调用remove()
方法等,造成内存泄漏。
DateTimeContextHolder
类,应用了ThreadLocal
ThreadLocal
的典型应用场景:每次HTTP
请求都对应一个线程,线程之间相互隔离RequestContextHolder
,也是用到了ThreadLocal
,看NamedThreadLocal
源码,再看getRequestAttributes
的调用