ThreadLocal = 本地线程?,
ThreadLocal = 本地线程?,
一、定义
ThreadLocal
是JDK
包提供的,从名字来看,ThreadLocal
意思就是本地线程的意思。
1.1 是什么?
要想知道他是个啥,我们看看ThreadLocal
的源码(基于JDK 1.8
)中对这个类的介绍:
This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable. {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).
大致能够总结出:
1.2 示例
一言不合上代码!
//创建ThreadLocal变量
private static ThreadLocal<String> localParam = new ThreadLocal<>();
@Test
public void threadLocalDemo() {
//创建2个线程,分别设置不同的值
new Thread(() -> {
localParam.set("Hello 风尘博客!");
//打印当前线程本地内存中的localParam变量的值
log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
}, "T1").start();
new Thread(() -> {
log.info("{}:{}", Thread.currentThread().getName(), localParam.get());
}, "T2").start();
}
- 结果:
... T1:Hello 风尘博客!
... T2:null
打印结果证明,T1
线程中设置的值无法在T2
取出,证明变量ThreadLocal
在各个线程中数据不共享。
1.3 ThreadLocal
的API
ThreadLocal
定义了四个方法:
set()
和initialValue()
区别
名称 | set() |
initialValue() |
---|---|---|
定义 | 为这个线程设置一个新值 | 该方法用于设置初始值,并且在调用get() 方法时才会被触发,所以是懒加载。但是如果在get() 之前进行了set() 操作,这样就不会调用 |
区别 | 如果对象生成的时机不由我们控制的时候使用 set() 方式 |
对象初始化的时机由我们控制的时候使用initialValue() 方式 |
二、实现原理
ThreadLocal
有一个特别重要的静态内部类ThreadLocalMap
,该类才是实现线程隔离机制的关键。
- 每个线程的本地变量不是存放在
ThreadLocal
实例里面,而是存放在调用线程的threadLocals
变量里面,也就是说:ThreadLocal
类型的本地变量存放在具体的线程内存空间中。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread
类中有两个ThreadLocalMap
类型的变量,分别是threadLocals
和inheritableThreadLocals
,而ThreadLocalMap
是一个定制化的Hashmap
,专门用来存储线程本地变量。在默认情况下,每个线程中的这两个变量都为null
,只有当前线程第一次调用ThreadLocal
的set()
或者get()
方法时才会创建它们。
ThreadLocal
就是一个工具壳,它通过set()
方法把value
值放入调用线程的threadLocals
里面并存放起来,当调用线程调用它的get()
方法时,再从当前线程的threadLocals
变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的
threadLocals
变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal
变量的remove()
方法,从当前线程的threadLocals
里面删除该本地变量。
另外Thread
里面的threadLocals
被设计为Map
结构是因为每个线程可以关联多个ThreadLocal
变量。
原理小结
三、使用场景
3.1 ThreadLocal
的作用
- 保存线程上下文信息,在任意需要的地方可以获取.
由于ThreadLocal
的特性,同一线程在某地方进行设置,在随后的任意地方都可以获取到。从而可以用来保存线程上下文信息。
- 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失.
3.2 场景一:独享对象
每个线程需要一个独享对象(通常是工具类,典型需要使用的类有SimpleDateFormat
和Random
)
这类场景阿里规范里面也提到了:
3.3 场景二:当前信息需要被线程内的所有方法共享
每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
演示(完整演示见文末Github)
User.java
@Data
public class User {
private String userName;
public User() {
}
public User(String userName) {
this.userName = userName;
}
}
UserContextHolder.java
public class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
Service1.java
public class Service1 {
public void process() {
User user = new User("Van");
//将User对象存储到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
Service2.java
public class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名: " + user.getUserName());
new Service3().process();
}
}
Service3.java
public class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名: " + user.getUserName());
}
}
- 测试方法
@Test
public void threadForParams() {
new Service1().process();
}
- 结果打印
Service2拿到用户名: Van
Service3拿到用户名: Van
3.4 使用ThreadLocal
的好处
四、问题
4.1 内存泄漏问题
内存泄露:某个对象不会再被使用,但是该对象的内存却无法被收回
- 正常情况
当Thread
运行结束后,ThreadLocal
中的value
会被回收,因为没有任何强引用了。
- 非正常情况
当Thread
一直在运行始终不结束,强引用就不会被回收,存在以下调用链
Thread-->ThreadLocalMap-->Entry(key为null)-->value
因为调用链中的 value
和 Thread
存在强引用,所以value
无法被回收,就有可能出现OOM
。
如何避免内存泄漏(阿里规范)
调用remove()
方法,就会删除对应的Entry
对象,可以避免内存泄漏,所以使用完ThreadLocal
后,要调用remove()
方法。
4.2 ThreadLocal
的空指针问题
ThreadLocalNPE.java
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
/**
* 当前返回值为基本类型,会报空指针异常,如果改成包装类型Long就不会出错
* @return
*/
public long get() {
return longThreadLocal.get();
}
}
- 空指针测试
@Test
public void threadLocalNPE() {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//如果get方法返回值为基本类型,则会报空指针异常,如果是包装类型就不会出错
System.out.println(threadLocalNPE.get());
}
如果get()
方法返回值为基本类型,则会报空指针异常;如果是包装类型就不会出错。这是因为基本类型和包装类型存在装箱和拆箱的关系,所以,我们必须将get()
方法返回值使用包装类型。
4.3 参考文章
四、技术交流
Github 示例代码
相关文章
- 暂无相关文章
用户点评