ThreadLocal

ThreadLocal 将一个变量隔离在某一个线程上,即该变量只在某个线程本身可见,其他线程无法访问,可以实现线程的安全性,而且可以避免线程同步带来的性能损失

image.png

如图所示,每个 Thread对象中都持有一个 ThreadLocalMap 成员变量,每个 ThreadLocalMap 有一个键值对 Entry[] table,可以认为是一个 map,ThreadLocal 就是这个键,我们需要存储的数据做为值

使用场景

每个线程需要一个独享的对象

通常是工具类,典型需要使用的有 SimpleDateFormat 和 Random
Java8 可以使用线程安全的 DateTimeFormatter 和 ThreadLocalRandom

1
2
3
4
5
6
public class DateUtil {

private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

public static ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat(DATE_FORMAT));
}
每个线程内需要保存全局变量

例如在拦截器中获取用户信息,可以直接让不同方法直接使用,避免参数传递的麻烦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ThreadLocalHolder {

private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

public static void set(User user) {
userHolder.set(user);
}

public static User get() {
return userHolder.get();
}

public static Integer getId() {
return userHolder.get().getId();
}

public static void remove() {
userHolder.remove();
}
}

public class ThreadLocalInterceptor implements HandlerInterceptor {

@Autowired
private RedisService redisService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String loginToken = request.getHeader(Const.HEAD_TOKEN);
if (StringUtils.isBlank(loginToken)) {
return false;
}
loginToken = Const.TOKEN_PREFIX + loginToken;
String userJsonStr = redisService.get(loginToken);
User user = JsonUtil.string2Obj(userJsonStr, User.class);
if (user == null) {
return false;
}
// 重置Session时间
redisService.expire(loginToken, Const.REDIS_SESSION_EXPIRE_TIME);
// 设置ThreadLocal
ThreadLocalHolder.set(user);
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ThreadLocalHolder.remove();
}
}

带来的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效的利用内存,节省开销
  • 免去传参的繁琐

主要方法介绍

  • initialValue:初始化,该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get() 的时候才会触发
    当线程第一次使用 get() 访问变量时,将调用此方法,除非线程之前调用了 set(),在这种情况下,不会为线程调用 initialValue()
    通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后,再调用 get() ,则可能会再次调用此方法
    如果不重写此方法,这个方法会返回 null,一般使用匿名内部类的方法来重写 initialValue(),以便在后续使用中可以初始化副本对象
  • set:为当前线程副本设置一个新值,set() 和 setInitialValue() 最后都是利用 map.set() 来设置值
  • get:得到当前线程副本对应的值,如果是首次调用 get(),则会调用 initialize 来得到这个值
  • remove:删除对应这个线程的值

内存泄露问题

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
value 的泄露

ThreadLocalMap 中的每一个 Entry 都是对 key 的弱引用(弱引用的特点是,如果这个对象只被弱引用关联,那么这个对象就可以被回收),同时每个 Entry 都包含了一个对 value 的强引用
正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了
但是,如果线程不终止(比如线程需要保持很久),那么 key 对应的 value 就不能被回收,所以有以下的调用链:Thread -> ThreadLocalMap -> Entry(key 为 null)-> value
因为 value 和 Thread 之间还存在这个强引用链路,所以导致 value 无法回收,就可能会出现 OOM
为了解决这个问题,JDK 会在 set、remove、rehash 方法中扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就会被回收
但是如果一个 TreadLocal 不被使用,那么实际上 set、remove、rehash 方法也不会被调用,如果同时线程又不停止,那么调用链就会一直存在,那么就导致了 value 的内存泄露

如何避免

调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄露,所以使用完 ThreadLocal 之后,应该调用 remove 方法