ThreadLocal
ThreadLocal 适用场景
场景 1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat、Random、工具类线程不安全,所以每个线程需要拥有自己独立的工具类)
* 每个 Thread 内有自己的实例副本,不共享
SimpleDateFormat 进化过程
1. 2个线程分别用自己的 SimpleDateFormat
2. 10个线程变为for循环,10个线程new 了10个SimpleDateFormat
3. 1000个线程,必然用线程池(否则消耗太多内存)
4. static SimpleDateFormat ,所有线程公用同一个 SimpleDateFormat 对象
(发现线程不安全)
5. 用 synchronized 加锁修复了线程安全性,但是影响了并发能力。
6. ThreadLocal 登场。线程池中的每个线程中,都有一个 SimpleDateFormat 对象副本,提高并发能力,还不会像 synchronized 那样带来线程安全问题
示例:两个线程分别打印出自己的日期 (用 synchronized 加锁,修复线程不安全问题)

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述: 加锁来解决线程安全问题
*/
public class ThreadLocalNormalUsage04 {
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 = new ThreadLocalNormalUsage04().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public String date(int seconds) {
//参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
return s;
}
}
示例:两个线程分别打印出自己的日期 (线程池中的每个线程中,都有一个 SimpleDateFormat 对象副本,提高并发能力,还不会像 synchronized 那样带来线程安全问题,有多少个线程只就会有多少个副本,线程复用依然使用同一个副本。)

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 描述: 利用 ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
*/
public class ThreadlocalNormalUsage666 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
HashSet<String> hashSet = new HashSet<>();
for (int i = 0; i < 1000; i++) {
int circleNum = i;
executorService.submit(new Runnable() {
@Override
public void run() {
String date = ThreadlocalNormalUsage666.date(circleNum);
System.out.println(date);
hashSet.add(date);
}
});
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println(hashSet.size());//1000
}
public static String date(int seconds ) {
////参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = ThreadLocal4SafeDateFormatter.simpleDateFormatThreadLocal.get();
String format = simpleDateFormat.format(date);
return format;
}
}
class ThreadLocal4SafeDateFormatter {
public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
//lambda的形式
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal4Lambda = ThreadLocal
.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
场景 2:每个线程内需要保存全局变量(例如在拦截器中获取用户的信息),可以让不同方法直接使用,避免参数传递的麻烦。
* 强调的是同一个请求(同一个线程内)不同方法间的共享
* 不需重写initialValue()方法,但是必须手动调用set()方法


/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("超哥");
UserContextHolder.holder.set(user);//设置到ThreadLocal中
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到用户名:" + user.name);//Service2拿到用户名:超哥
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);//Service3拿到用户名:超哥
UserContextHolder.holder.remove();//避免ThreadLocalMap中的Entry内存泄漏。
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
ThreadLocal 作用
- 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象),在固定线程数的每个线程中有独立的副本对象(节省内存),同时避免了 synchronized 带来并发性能下降的问题。
- 同一个请求内(一次请求对应一个线程),任何方法都可以获取到该对象,避免传参的繁琐。
initialValue 适用的场景
在 ThreadLocal 第一次 get 的时候把对象初始化出来,对象的初始化时机可以由我们控制。
set 适用的场景
如果需要保存到 ThreadLocal 里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用 ThreadLocal.set 直接放到我们的 ThreadLocal 中去,以便后续使用。
ThreadLocal 带来的好处
- 达到线程安全(static 共享并不能做到线程安全)
- 不需要加锁、提高了并发效率
- 更搞笑地利用内存、节省开销:相比于每个任务都新建一个 SimpleDateFormat,显然用 ThreadLocal 可以节省内存和开销。
- 免去传参的繁琐(无论是场景一的工具类、还是场景二的用户,都可以在任何地方直接通过 ThreadLocal 拿到,再也不需要每次都传同样的参数。Thread 使得代码耦合度更低,更优雅)
ThreadLocal 原理
Thread、ThreadLocal、ThreadLocalMap 三者之间的关系
- 每个 Thread 对象中都持有一个 ThreadLocalMap 成员变量。
- 一个 ThreadLocalMap 中包含多个 ThreadLocal 对象。

initialValue()
- 该方法会返回当前线程对应的“初始值”,这是个延迟加载的方法,只有在调用 get() 的时候,才会触发。
- 当前线程第一次使用 get() 方法访问变量时,将调用此方法, 除非线程先前调用了 set() 方法, 在这种情况下,不会为线程调用本 initialValue() 方法。
- 通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后,再调用 get(),则可以再次调用此方法。
- 如果不重写本方法,这个防范会返回 null。一般使用匿名内部类的防范来重写 initialValue() 方法,一遍在后续使用中可以初始化副本对象。
void set(T t)
为这个线程设置一个 ThreadLocal 对象的值。
T get()
得到这个线程对应 ThreadLocal 的 value。 如果是首次调用 get(),则会调用 initialize() 来得到这个值(懒加载)。
void remove()
删除对应这个线程中 ThreadLocal 对象的值。
两种场景殊途同归
setInitialVlaue()、set() 最后都是利用 map.set() 方法来设置值。最后都会对应到 ThreadLocalMap 的一个 Entry ,只不过是起点和入口不一样。
ThreadLocal 注意点
内存泄漏
内存泄漏:某个对象不再有用,但是占用的内存却不能被回收。
ThreadLocalMap 的每个 Entry 都是一个对 key 的弱引用,同时,每个 Entry 都包含了一个对 value 的强引用。 一个线程长时间被复用,导致其 ThreadLocalMap 中会有很多遗留无法回收的 Entry。
JDK 已经考虑到了这个问题,所以在 set、remove、rehash 方法中会扫描 key 为 null 的 Entry,并吧对应的 value 设置成 null,这样 value 对象就可以被回收。
但是如果一个 ThreadLocal 不被使用,那么实际上 set,remove,rehash 防范也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了 value 的内存泄漏。
调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄露,所以使用完 ThreadLocal 之后,应该调用 remove 方法。
# 线程池复用线程(某线程长时间被复用),以下调用链会导致内存泄漏:
Thread --> ThreadLocalMap --> Entry(key为null) --> Value (强引用:无法被GC回收)
ThreadLocal 空指针异常
抛出异常是因为类型转换,null 无法从包装类转换成基本类型。get 本身只会返回 null,并不会抛出异常。
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
/**
* 装箱拆箱 Long -> long
* @return
*/
public long get() {
return longThreadLocal.get();//null 无法转换成long
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
System.out.println(threadLocalNPE.get());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
}
});
thread1.start();
}
}
共享对象
如果在每个线程中 ThreadLocal.set() 进去的东西本身就是多个线程国祥的对象(比如 static 对象),那么多个线程的 ThreadLocal.get() 取得的还是这个共享对象本身,依然存在并发问题。不应该在 ThreadLocal 中存放静态对象。
如果可以不使用 ThreadLocal 解决问题,那么不要强行使用
例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要用到 ThreadLocal。
优先使用框架的支持,而不是自己创造
例如在 Spring 中,如果可以使用 RequestContextHolder,那么就不需要自己维护 ThreadLocal,因为自己可能会忘记调用 remove() 方法等,造成内存泄漏。
DateTimeContextHolder、RequestContextHolder ……- 每次 HTTP 请求都对应一个线程,线程之间相互隔离,这就是 ThradLocal 的典型应用场景。