# 二、ThreadLocal
# 1. 两大使用场景
# 1.1 场景一:initialValue
每个线程都需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random)。
- 每个 Thread 内有自己的实例副本,不共享。
- 线程安全,因为每个线程都是用自己独享的 SimpleDateFormat
- 只创建了 10 个 SimpleDateFormat 对象,这样就不用创建 1000 个,减少了内存损耗。
- 不需要加锁,效率高。
public class ThreadLocalCaseOne{
//线程池
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
/**
* 利用传进来的秒数生成一个时间
*
* @param seconds
* @return
*/
public String date(int seconds){
//获取 ThreadLocal 中的 SimpleDateFormat
SimpleDateFormat sdf = ThreadSafeFormatter.dateFormatThreadLocal.get();
return sdf.format(new Date(seconds * 1000));
}
public static void main(String[] args) {
//1000个打印日期的任务,用线程池
for (int i = 0; i < 1000; i++) {
int second = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println(new ThreadLocalCaseOne().date(second));
}
});
}
}
}
// ThreadLocal
class ThreadSafeFormatter{
//每个线程都有自己单独一份 SimpleDateFormat
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
//※ 重写 initialValue() 方法
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
}
# 1.2 场景二:set
同一次请求(同一个线程)内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
- 无需 synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可以达到保证当前线程获取对应的用户信息的目的。
- 每个线程独享自己的 User 对象,同一个线程之间维护一个上下文(Holder),方便信息传递。
/**
* 利用 ThreadLocal 来实现同一个线程内不同方法的参数传递
*
* @author Hedon Wang
* @create 2021-03-17 8:25 PM
*/
public class ThreadLocalCaseTwo {
public static void main(String[] args) {
new Service1().process();
}
}
/**
* 第一层处理
*/
class Service1{
public void process(){
User user = new User("Hedon");
//放入用户信息上下文
UserContextHolder.threadLocal.set(user);
//往后调用
new Service2().process();
}
}
/**
* 第二层处理
*/
class Service2{
public void process(){
//获取用户上下文中用户信息
User user = UserContextHolder.threadLocal.get();
System.out.println("service2 获得的用户信息:" + user);
//往后调用
new Service3().process();
}
}
/**
* 第三层处理
*/
class Service3{
public void process(){
//获取用户上下文中用户信息
User user = UserContextHolder.threadLocal.get();
System.out.println("service3 获得的用户信息:" + user);
}
}
/**
* 用户信息上下文:ThreadLocal
*/
class UserContextHolder{
public static ThreadLocal<User> threadLocal = new ThreadLocal<>();
}
/**
* 用户类
*/
class User{
String name;
public User(String name){
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
结果:与 Service1 同属一个线程的 Service2 和 Service3 都可以拿到 Service1 放进去的 User 信息。
对比:
场景一:initialValue | 场景二:set |
---|---|
在 ThreadLocal 第一个 get 的时候把对象初始化出来,对象的初始化时机可以由我们控制。 | 如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用 ThreadLocal.set 直接放到我们的 ThreadLocal 中,以便我们后续使用。 |
# 2. 作用
- 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象),且在任何方法中可以轻松地获取该对象。
- 线程安全。
- 不需要加锁,提高执行效率。
- 更高效地利用内存、节省开销。
- 免去传参的繁琐,降低代码耦合度。
# 3. 源码
- 一个 Thread 对应一个 ThreadLocalMap。
- 一个 ThreadLocalMap 存储很多个 ThreadLocal。
- ThreadLocal 其实就一个 key-value 键值对,key 是 ThreadLocal,value 是任何对象。
# 3.1 T initialValue()
private T setInitialValue() {
//调用 initialValue() 方法
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//key:this(ThreadLocal),value: T(initialValue 中创建的对象)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
//默认返回 null
return null;
}
- 因为 initialValue() 默认返回一个 null,所以我们上述场景一需要重写 initialValue() 方法,让它来为我们生成我们想要的对象。
其他注意点:
- 该方法回返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get() 的时候,才会触发。
- 当线程第一次使用 get() 方法访问变量时,将调用次方法,除非线程先调用了 set() 方法,在这种情况下,不会为线程调用本 initialValue() 方法。
- 通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后(ThreadLocalMap 没了),再调用 get(),则可以再次调用此方法。
# 3.2 set()
public void set(T value) {
//1. 获取当前线程
Thread t = Thread.currentThread();
//2. 得到当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//4. 之后 set 的时候就把 value 放入 map 中
map.set(this, value);
else
//3. 第一次调用为 map 为空,所以会创建一个 map
createMap(t, value);
}
# 3.3 get()
public T get() {
//1. 获取当前线程
Thread t = Thread.currentThread();
//2. 得到当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
//3-1 如果之前有调用过 set(),那 map 就不为空,就执行下面的 if 语句
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//3-2 如果之前没有调用过 set(),那就会调用 setInitialValue(),而 setInitialValue() 里面又调用了 initialValue()
return setInitialValue();
}
# 3.4 remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
//删除 map
if (m != null)
m.remove(this);
}
ThreadLocalMap.remove()
/**
* 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)]) {
//找到对应的 key (threadLocal) 的 value,然后删除它们
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
# 3.5 ThreadLocalMap
- Hash 冲突的时候不采用拉链法,而且继续往后找空位。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//键值对
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
# 4. 注意点
# 4.1 内存泄漏
正常情况下,当线程终止时,保存在 ThreadLocal 里的 value 会被 GC,因为没有任务强引用了。
但是如果使用线程池的话,线程不会终止,那么就存在:
- Thread -> ThreadLocalMap -> threadLocal -> value
- 这里 threadLocal 是一种 WeekReference 弱引用,会被 GC。
- 所以唯一可能滞留在内存中不会被 GC 的就是强引用的 value,那就有可能造成 OOM。
JDK 已经考虑了这个问题,所以在 set、remove、rehash 方法中会扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就可以被回收。
if (k == null) { replaceStaleEntry(key, value, i); return; }
但是如果一个 ThreadLocal 不被使用,那么实际上 set、remove、rehash 方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就有可能导致 value 的内存泄露。
避免:(阿里规约)
- 在线程使用 ThreadLocal 的最后一个方法执行后调用 remove() 方法删除对应的 Entry 对象。
# 4.2 空指针异常
- 没有 set() 也没有重写 initialValue() 的情况下直接 get(),如果存储在 ThreadLocal 中的对象是基本数据类型的封装类,在接受对象的时候使用基本数据类型来接收,比如用 long 来接收 Long,那么一个 null 的 Long 在自动拆箱的时候就会报 NullPointerException。
# 4.3 共享对象
- 如果在每个线程中 ThreadLocal.set() 进去的东西本身就是多线程共享的同一个对象,比如 static 对象,那么多个线程的 ThreadLocal.get() 取得的对象还是这个共享对象本身,而不是每个线程独享的,那么还是有并发访问问题。
# 4.4 使用需要
- 没必要不使用,麻烦。
- 优先使用框架的支持,而不是自己创建。
# 5. ThreadLocal 在 Spring 中的应用
- DateTimeContextHolder<DateTimeContext>:存时间
- RequestContextHolder<RequestAttributes>:存请求参数