1. ThreadLocal简介
1.1 ThreadLocal是什么?
定义:提供线程局部变量,一个线程局部变量在多个线程中,分别有独立的值(副本)。
ThreadLocal又叫做线程变量
,意思是ThreadLocal中填充的变量
属于当前线程
,该变量对其他线程而言是隔离
的,也就说该变量是当前线程独有的变量
。ThreadLocal为变量在每一个线程中都创建了一个副本。那么每个线程都可以访问自己内部的副本变量。
同一个ThreadLocal所包含的对象,在不同的Threa中有不同的副本,这里有几点需要注意:
- 因为每个Thread内有自己的实例副本,
且该副本只能由当前Thread使用
,这也是ThreadLocal命名的由来。 - 既然每个Thread都有自己的实例副本,且其他Thread不可访问,
那就不存在多线程共享的问题
。
ThreadLocal提供了线程本地的实例,它与普通变量的区别在于:
- 每个使用该变量的线程都会初始化一个完全独立的实例副本。
- ThreadLocal变量通常被
private static
修饰。 - 当一个线程结束时,它所使用的所有ThreadLocal相对的实例副本都可被回收。
总的来说,ThreadLocal适用于每个线程需要自己独立的实例
,且该实例需要在多个方法中被使用
。即变量在线程间隔离,但是在方法和类间共享。
ThreadLocal在使用过程中状态
从图中可以看出,每个Thread
对象都有一个ThreadLocalMap
,每个ThreadLocalMap
可以存储多个ThreadLocal
1. 2 ThreadLocal常见使用场景
如上文所述,ThreadLocal适用于以下两种场景:
每个线程需要有自己单独的实例。
实例需要在多个方法中共享,但不希望被多线程共享。
对于第一点,每个线程拥有自己的实例,实现它的方式有很多,例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现,ThreadLocal 使得代码耦合度更低,且实现更优雅。
场景一:每个线程需要一个独享的对象(通常是工具类,工具类型需要使用的类有
SimpleDateFormat
和Random
)。
public class ThreadLocalDemo {
//创建一个核心线程为10的定长线程池
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//格式化时间
public static String date(int seconds){
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date=new Date(seconds*1000);
//日期格式
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
public static void main(String[] args) {
//启动1000个线程,新建1000个SimpleDateFormat对象
for (int i = 0; i <1000 ; i++) {
int finalI = i;
threadPool.submit(()->{
String date = date(finalI);
System.out.println(date);
});
}
//关闭线程池
threadPool.shutdown();
}
}
这里虽然使用了线程池,但是1000个线程在执行过程中都创建一个SimpleDateFormat
对象,这比较耗费内存资源。那么如何改进呢?
改进一:将SimpleDateFormat
对象提取出来用static
修饰符修饰,这样每一个线程都可以共用一个SimpleDateFormat对象,减少内存消耗,但是这样会打印出相同的时间,所有线程都在争夺这个资源,我们需要一个锁去控制,避免出现线程安全问题。
//日期格式
static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
改进二:上图可以看到出现了两个相同的时间,因此在改进一的基础上添加锁控制,代码如下:
public class ThreadLocalDemo {
//创建一个核心线程为10的定长线程池
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//日期格式
static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//格式化时间
public static String date(int seconds){
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date=new Date(seconds*1000);
String str = null;
synchronized (ThreadLocalDemo.class){
str= sdf.format(date);
}
return str;
}
public static void main(String[] args) {
//启动1000个线程,新建1000个SimpleDateFormat对象
for (int i = 0; i <1000 ; i++) {
int finalI = i;
threadPool.submit(()->{
String date = date(finalI);
System.out.println(date);
});
}
//关闭线程池
threadPool.shutdown();
}
}
这虽然能够满足要求,但是在高并发场景下,所有线程需要一个个的去获取锁,需要排队等待,这显然性能损耗太大。
改进三:使用ThreadLocal
(不仅线程安全,而且也没有synchronized
带来的性能问题,每个线程内有自己独享的SimpleDateFormat对象)。
class ThreadLocalSimpleDateFormat{
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"));
}
// 利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
public class ThreadLocalDemo {
//创建一个核心线程为10的定长线程池
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//格式化时间
public static String date(int seconds){
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date=new Date(seconds*1000);
// 拿到initialValue返回对象
SimpleDateFormat dateFormat = ThreadLocalSimpleDateFormat.dateFormatThreadLocal.get();
System.out.println(dateFormat);
return dateFormat.format(date);
}
public static void main(String[] args) {
//启动1000个线程,新建1000个SimpleDateFormat对象
for (int i = 0; i <6 ; i++) {
int finalI = i;
threadPool.submit(()->{
String date = date(finalI);
System.out.println(date);
});
}
//关闭线程池
threadPool.shutdown();
}
}
像这种需要每个线程内独享的对象,一般使用场景是工具类中。后面再讲解原理,讲讲每个线程为什么都有独享的对象,这里先看用法。
场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
比如当一个请求进来了,一个线程负责处理该请求,该请求会依次调用service-1()
, service-2()
, service-3()
,同时,每个service()
都需要获得调用方用户user
的信息,也就是需要拿到user对象
。
一个比较繁琐的解决方案是把user
作为参数层层传递,从service-1()
传到service-2()
,再从service-2()
传到service-3()
,以此类推,但是这样做会导致代码冗余且不易维护。
在此基础上可以演进,使用UserMap
,就是每个用户的信息都存在一个Map中,当多线程同时工作时,我们需要保证线程安全,可以用synchronized
也可以用ConcurrentHashMap
,但这两者无论用什么,都会对性能有所影响。
有没有更好的方法呢?ThreadLocal就来了
//用户类
class User {
String name;
Integer age;
public User(String name,Integer age){
this.name = name;
this.age=age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
class UserUtil{
private static ThreadLocal<User> holder=new ThreadLocal<>();
public static void setParamter(User user){
holder.set(user);
}
public static User getParamter( ){
return holder.get();
}
public static void remove( ){
holder.remove();
}
}
class Service1 {
public void service1(User user) {
//给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
UserUtil.setParamter(user);
new Service2().service2();
}
}
class Service2 {
public void service2(){
User user = UserUtil.getParamter();
System.out.println("service2拿到的用户:"+user);
new Service3().service3();
}
}
class Service3 {
public void service3() {
User user = UserUtil.getParamter();
System.out.println("service3拿到的用户:" + user);
//在整个流程执行完毕后,一定要执行remove
UserUtil.remove();
}
}
public class ThreadLocalDemo2 {
public static void main(String[] args) {
User user=new User("tomy",23);
new Service1().service1(user);
}
}
测试结果:
这样,不管哪个Service
都能拿到User
对象,能获取User
对象内的所有信息。并且假如有多个请求,一个张三,一个李四,因为他们并没有直接共享User
对象,所以他们之间不会有线程安全问题。
2. ThreadLocal的作用和好处
2.1 ThreadLocal的两个作用
- 在任何方法中都可以轻松获取到该对象。
- 让某个需要用的对象在
线程间隔离
(每个线程都有自己独立的对象)
2.2 ThreadLocal两种用法
根据共享对象的生成时机不同,选择initialValue()
和set()
方法来保存对象。
- 不受外界传参影响时,可以选择重写
initialValue()
方法来初始化保存对象,会在ThreadLocal
第一个调用get()
方法的时候初始化对象,对象初始化的时机可以由我们控制,比如上面第一个例子工具类。 - 如果需要保存到
ThreadLocal
里的对象的生成时机不由我们随意控制
,例如拦截器生成的用户信息,用ThreadLocal
中的set()
方法直接放到我们的ThreadLocal
里面去,以便后续使用。对应代码就是上面第二个例子。
2.2 使用ThreadLocal带来的四个好处
- 不需要加锁,提高执行效率
- 线程安全
- 更高效地利用内存节省开销,上面例子中,相比于成千上万个任务,每个任务都新建一个
SimpleDateFormat
,显然用ThreadLocal
可以节省内存和开销。 - 免去传参的繁琐,不需要每次都传同样的参数,
ThreadLocal
使得代码耦合度更低,更优雅
3. ThreadLocal主要方法介绍
主要有initialValue()
,set()
,get()
和remove()
这几个方法,关于源码分析,将在第4节介绍。
initialValue()
方法会返回当前线程对应的初始值
,这是一个延迟加载的方法,只有在调用get()
方法的时候才会触发。- 如果不重写
initialValue()
方法,这个方法会返回null
,一般使用匿名内部类的方法重写initialValue()
方法,以便在后续的使用中可以初始化副本对象。 - 当线程
第一次调用get()方法
访问变量的时候,会调用initialValue()
方法,除非线程先前调用了set()
方法,在这种情况下,不会为线程调用本initialValue()
。 - 通常,每个线程最多调用一次
initialValue()
方法,但如果已经调用一次remove()
方法后,再调用get()
方法,则可以再次调用initialValue()
,相当于第一次调用get()
。
4. ThreadLocal原理源码分析
4.1 get()方法
//获取
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap,如果之前调用过set方法,那么这里getMap就不为null
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取ThreadLocalMap存储的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// 调用过set会从这里return
return result;
}
}
// 如果当前线程还没有创建ThreadLocalMap,执行setInitialValue方法去初始化
return setInitialValue();
}
// getMap就是看看当前线程有没有创建ThreadLocalMap集合,如果没有,这个集合就是为null
ThreadLocalMap getMap(Thread t) {
//获取当前线程的threadLocals属性
return t.threadLocals;
//ThreadLocal.ThreadLocalMap threadLocals = null;
}
private T setInitialValue() {
//调用你重写的initialValue获取返回值
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
/**
*只有第一次使用get()才调用initialValue()的原因是:
*第一次创建ThreadLocalMap第二次及以后,getMap发现ThreadLocalMap不是null,走不到这个方法来了。
*set存的key是什么?this是当前ThreadLocal对象!
*/
map.set(this, value);
else
//创建一个ThreadLocalMap对象
createMap(t, value);
return value;
}
//创建一个ThreadLocalMap对象,并把引用指向线程的threadLocals
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
总结:
- get()方法:先取出当前线程对象的
ThreadLocalMap
,如果ThreadLocalMap
不为空,则调用map.getEntry()
方法,把本ThreadLocal
的引用作为参数传入,取出map
中属于本ThreadLocal
的value
。,如果ThreadLocalMap
为空,则调用setInitialValue()
方法。 - getMap():获取当前线程内的
ThreadLocalMap
对象,每个线程内都有ThreadLocalMap
对象,名为threadLocals
,初始值为null
。 - setInitialValue():先去调用
initialValue()
方法(如果没有重写为null,可以使用匿名内部类的方式对其初始化),然后调用getMap()
获取ThreadLocalMap
对象,如果不为空则调用map.set(this, value);
去设置值,反之调用createMap(t, value);
去初始化一个ThreadLocalMap
对象。 - 注意:这个
map
和map
中的key
和value
都是保存在线程中ThreadLocalMap
的,而不是保存在ThreadLocal
中。
4.2 set()方法
// 把当前线程需要全局共享的value传入
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程中的属性ThreadLocalMap ,
ThreadLocalMap map = getMap(t);
// map对象为空就创建,不为空就覆盖
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
总结:
- set():先取出当前线程对象的
ThreadLocalMap
,如果ThreadLocalMap
不为空,则直接更新value
值,如果为空调用createMap()
去初始化一个ThreadLocalMap
对象。并将value初始化。 - 上面也对
createMap()
进行分析,发现是新建ThreadLocal的一个静态内部类对象。
//创建一个ThreadLocalMap对象,并把引用指向线程的threadLocals
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
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);
}
可看出ThreadLocalMap
是ThreadLocal
的内部静态类
,而它的构成主要是用Entry
来保存数据 ,而且还是继承的弱引用
。在Entry
内部使用ThreadLocal
作为key
,使用我们设置的value
作为value
。详细内容要大家自己去跟。
4.3 initialValue()方法
这个方法没有默认实现,如果要用initialValue()
方法,需要自己实现,通常使用匿名内部类
的方式实现(可以回顾上面代码)
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal=new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
4.4 remove()方法
// 删除对应这个线程的值
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 移除这个ThreadLocal对应的值
m.remove(this);
}
- remove():直接将
ThreadLocal
对应的值从当前线程的ThreadLocalMap
中删除,为什么要删除呢,这涉及到内存泄漏的问题。 - 实际上
ThreadLocalMap
中使用的key
为ThreadLocal
的软引用
,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉
。 - 所以如果
ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来ThreadLocalMap
中使用这个ThreadLocal
的key
也会被清理掉。但是,value
是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
4.5 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内存泄漏问题
什么是内存泄漏?
某个对象不再有用,但是占用的内存却不能被回收,时机久了,内存中存储的数据越来越多,就会导致内存溢出(OOM)。
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<?>> {
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已经考虑到了这个问题,所以在
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。
5.2 ThreadLocal如何避免内存泄漏?
使用结束后及时调用remove()
方法,删除对应的Entry
对象,可以避免内存泄漏,所以使用完ThreadLocal
之后,应该调用remove()
方法。
比如使用拦截器获取到用户信息,用户信息保存在ThreadLocalMap
中,线程请求结束之前拦住它,并用remove()
清除User
对象,这样就能保证不会内存泄漏。
5.3 共享对象问题
如果在每个线程中ThreadLocal.set()
进去的对象本来就是多线程共享的同一个对象,比如static对象,那么多个线程 ThreadLocal.get()
取得得还是这个共享对象本身,还是有并发访问问题。
5.4 优先使用框架的支持,而不是自己创造
在Spring中,如果可以使用RequestContextHolder
,那么就不需要自己维护ThreadLocal
,因为自己可能会忘记调用remove()
方法等,造成内存泄漏。
5.5 在Spring中实例中哪里用到了ThreadLocal?
- DateTimeContextHolder类,应用了ThreadLocal。
- ThreadLocal的典型应用场景:每次HTTP请求都对应一个线程,线程之间相互隔离。
- 看RequestContextHolder,也是用到了ThreadLocal,看NamedThreadLocal源码,再看getRequestAttributes的调用
5.6 ThreadLocal的实例以及其值存放在堆还是栈上?
- 在java中,栈内存属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属的线程可见,即栈内存可以理解为线程的私有内存。
- 堆内存中的对象对所有线程是可见的,堆内存中的对象可以被所有线程访问。
- 有人可能会说,ThreadLocal的实例以及其值存放在栈上,理由是是只能当前线程访问,其他线程访问不到。其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有)。而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
5.7 ThreadLocal的数据可以在其他线程共享吗?
是可以的,只不过只能在主线程和子线程之间共享
,也就是父线程和子线程之间可以共享数据。使用InheritableThreadLocal
可以实现多个线程访问ThreadLocal
的值,我们在主线程中创建一个InheritableThreadLocal
的实例,然后在子线程中得到这个InheritableThreadLocal
实例设置的值。
public class InheritableThreadLocalDemo {
//创建一个InheritableThreadLocal实例,在多个线程线程共享
private static ThreadLocal<String> threadLocal=new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("我是主线程");
Thread t=new Thread(()->{
System.out.println("子线程获取主线程的值: "+threadLocal.get());
});
//启动线程
t.start();
}
}
测试结果:
在子线程中我是能够正常输出那一行日志的,这也是我之前提到过的父子线程数据传递的问题。
5.8 InheritableThreadLocal是怎么传递数据的呢?
传递的逻辑很简单,我在开头Thread
代码提到threadLocals
的时候,你们再往下看看就会发现放了另外一个变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread源码中,我们看看Thread.init()初始化创建的时候做了什么:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//省略其他代码
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
- 我就截取了部分代码,如果线程的inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。