ThreadLocal的作用
- 让某个需要用到的对象在线程间隔离(每 个线程都有自己的独立的对象)
- 在任何方法中都可以轻松获取到该对象
ThreadLocal的两大使用场景
- 典型场景1 :每个线程需要一个独享的对象 (通常是工具类,典 型需要使用的类有SimpleDateFormat和Random -- 这些工具类都是线程不安全的)
- 典型场景2 :每个线程内需要保存全局变量(例如在拦截器中获 取用户信息) , 可以让不同方法直接使用,避免参数传递的麻烦
场景一:每个线程需要一个独享的对象
每个Thread内有自己的实例副本,不共享。
比喻:教材只有一-本,一起做笔记有线程安全问题。复印后没问题
- 每次调用创建一个新对象(SimpleDateFormat为线程独占独享) -- 线程安全
public class ThreadLocalNormalUsage00 {
public static ExecutorService executorService =
Executors.newFixedThreadPool(10);
public static void main(String[] args) {
ThreadLocalNormalUsage00 threadLocalNormalUsage00 = new ThreadLocalNormalUsage00();
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(threadLocalNormalUsage00.date(finalI));
}
});
}
executorService.shutdown();
}
public String date(int seconds) {
// 参数是从1970.1.1 00:00开始
Date date = new Date(1000*seconds);
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd hh:mm:ss");
return format.format(date);
}
}
- 使用类变量/类实例变量SimpleDateFormat -- 线程不安全
这两种变量都存在于Java运行时数据区域的线程共享区
public class ThreadLocalNormalUsage01 {
public static ExecutorService executorService =
Executors.newFixedThreadPool(10);
public static void main(String[] args) {
ThreadLocalNormalUsage00 threadLocalNormalUsage00 = new ThreadLocalNormalUsage00();
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(threadLocalNormalUsage00.date(finalI));
}
});
}
executorService.shutdown();
}
// static SimpleDateFormat format = new SimpleDateFormat(
// "yyyy-MM-dd hh:mm:ss");
SimpleDateFormat format = new SimpleDateFormat(
"yyyy-MM-dd hh:mm:ss");
public String date(int seconds) {
// 参数是从1970.1.1 00:00开始
Date date = new Date(1000*seconds);
return format.format(date);
}
}
- 使用ThreadLocal(为每个线程创建SimpleDateFormat副本) -- 线程安全
public class ThreadLocalNormalUsage02 {
public static ExecutorService executorService =
Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
ThreadLocalNormalUsage01 threadLocalNormalUsage00 = new ThreadLocalNormalUsage01();
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(threadLocalNormalUsage00.date(finalI));
}
});
}
executorService.shutdown();
}
public String date(int seconds) {
// 参数是从1970.1.1 00:00开始
Date date = new Date(1000*seconds);
SimpleDateFormat simpleDateFormat = threadLocal.get();
// 输出的类实例是一样的
System.err.println(simpleDateFormat);
return simpleDateFormat.format(date);
}
public static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
场景二:每个线程内需要保存全局变量
- 强调的是同一个请求内(同一个线程内)不同方法间的共享
- 不需重写initialValue()方法,但是必须手动调用set()方法
@Controller
@RequestMapping("/threadLocal")
public class ThreadLocalController {
@RequestMapping("/test")
@ResponseBody
public Long test() {
return RequestHolder.getId();
}
}
public class RequestHolder {
private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();
/**
* 在请求即将进入后端服务器之前进行调用
*
* @param id
*/
public static void add(Long id) {
requestHolder.set(id);
}
public static Long getId() {
return requestHolder.get();
}
/**
* 接口处理完之后进行
* 如果不进行释放,将会造成内存泄露
*/
public static void remove() {
requestHolder.remove();
}
}
场景使用选择
根据共享对象的生成时机不同,选择
initialValue或set来保存对象
场景一:initialValue 在ThreadLocal第一次get的时候把对象给初始化出来 ,对象的初始化时机可以由我们控制
场景二:set 如果需要保存到ThreadLocal里的对象的生成时机不由我们 随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便 后续使用。
使用ThreadLocal带来的好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 更高效地利用内存、节省开销:相比于每个任务都新建一个 SimpleDateFormat,显然用ThreadLocal可以节省内存和开 销
- 免去传参的繁琐:无论是场景一的工具类 ,还是场景二的用户 名,都可以在任何地方直接通过ThreadLocal拿到,再也不需 要每次都传同样的参数。ThreadLocal使得代码耦合度更低, 更优雅
ThreadLocal原理
Thread,ThreadLocal,ThreadLocalMap之间的关系


每个Thread对象中都持有一个ThreadLocalMap成员变量
ThreadLocal中的方法
- get():返回此线程局部变量的当前线程副本中的值。
- initialValue():返回此线程局部变量的当前线程的“初始值”。这是一个延迟加载的方法,只有调用
get()的时候,才会触发 - remove():移除此线程局部变量当前线程的值。
- set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。
ThreadLocal存在的问题
- 空指针异常
ThreadLocal<Long> LongThreadLocal = new ThreadLocal<Long>();
public void set() {
longThreadLocal.set(Thread. currentThread().getId() ) ;
}
// 注意,要将long改为Long
public long get() {
return LongThreadLocal.get() ;
}
如果没有set值的话,上面的
get()方法会出现空指针异常。因为ThreadLocal指定了泛型,如果是get的返回值为基本类型的话,会进行一次拆箱操作,如果get结果为null,则会抛出异常。
-
并发问题 如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程;的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
-
内存泄漏问题
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。
Java为了最小化减少内存泄露的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最怕的情况就是,threadLocal对象设null了,开始发生“内存泄露”,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄露。
- 参考课程:慕课网 - 玩转Java并发工具,精通JUC,成为并发多面手