-
ThreadLocal 表示线程的局部变量,当前线程可以通过 set/get 来对这个局部变量进行操作,其他线程不能对其进行访问
-
ThreadLocal 支持泛型,也就是支持指定 value 类型,像是
ThreadLocal<Date>
就是指定 value 为 Date 类型。 -
每个线程会有一份私有的 ThreadLocalMap 变量(threadLocals),去储存这个线程自己想存放的 ThreadLocal 变量们。
public class Thread implements Runnable { //Thread 类里的 threadLocals 存放此线程的专有的 ThreadLocalMap ThreadLocal.ThreadLocalMap threadLocals = null; }
ThreadLocalMap 可以理解为是一个 Map,Map 的 key 是指向某个 ThreadLocal 对象的引用,value 就是这个线程自己 set 的值。实际上,ThreadLocalMap 中保存有一个 Entry 集合,Entry 中用两个域分别保存 key 和 value。
-
对于一个线程来说,一个 ThreadLocal 只能关联一个值,而一个线程可以关联多个 ThreadLocal。
下面有个例子,在 main 线程中的 ThreadLocalMap,就有两个 key-value 的映射,分别是 userIdThreadLocal -> 100、userNameThreadLocal -> hello。
public class Main { public static void main(String[] args){ ThreadLocal<Integer> userIdThreadLocal = new ThreadLocal<>(); ThreadLocal<String> userNameThreadLocal = new ThreadLocal<>(); userIdThreadLocal.set(100); userNameThreadLocal.set("hello"); } }
-
当调用 ThreadLocal
tl
的tl.get()
方法时,其实就是先去取得此线程的 ThreadLocalMap,然后再去查找这个 Map 中的 key 为 的tl
那个 Entry 的 value 值。
-
-
ThreadLocal 常用的方法
-
set(x)
: 设置此线程局部变量的想要放的值是多少[1]; -
get()
: 取得此线程局部变量当初存放的值,如果没有存放过则返回 null; -
remove()
: 删除此线程局部变量的键值对,也就是如果先执行 remove 再执行 get,会返回 null。
-
-
ThreadLocal 可以用在 SimpleDateFormat,或是 SpringMVC 上
-
因为 SimpleDateFormat 不是线程安全的,虽然可以每次要使用的时候重新
new
一个,但是这样做会很浪费资源,所以如果使用 ThreadLocal 在每个线程里都存放一个此线程专用的 SimpleDateFormat,就可以避免一直new
的资源浪费,同时又确保线程安全。 -
因为 SpringMVC 会对每个请求分配一个线程,可以在拦截器将此线程的用户信息(ip、名字…)使用 ThreadLocal 储存,这样在后续要用到用户信息的地方时,就可以去 ThreadLocal 中取得,而且因为 ThreadLocal 可以隔离线程,因此每条请求对应的线程的用户信息不会互相干扰。
-
-
ThreadLocal 可能造成的内存泄漏
之所以 ThreadLocal 会发生内存泄漏,原因是因为只要线程活着,这个线程的 ThreadLocalMap 就会一直活着,而当初透过 ThreadLocal set() 的值,也就会在 ThreadLocalMap 中一直存在,也就是该 ThreadLocal 和该 value 的内存地址始终都有这个 ThreadLocalMap 在引用着(被 ThreadLocalMap 中的 Entry 直接引用),导致 GC 无法回收它,所以才会发生内存泄漏。
为了解决这个问题,Java 做了一个小优化,在 ThreadLocalMap 中使用弱引用来指向 ThreadLocal,如果一个 ThreadLocal 没有外部强引用来引用它,只有这条 ThreadLocalMap 的弱引用来引用它时,那么当系统 GC 时,这些 ThreadLocal 就会被回收(因为是弱引用),如此一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry 们。
public class ThreadLocal<T> { //根据线程,取得那个线程自己的 ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } static class ThreadLocalMap { //ThreadLocalMap 的 key 是使用“弱引用”的 ThreadLocal static class Entry extends WeakReference<ThreadLocal> { Object value; //ThreadLocalMap 中的 key 就是 ThreadLocal,value 就是设置的值 Entry(ThreadLocal k, Object v) { super(k); value = v; } } } }
下图中,实线表示强引用,虚线表示弱引用:
这个弱引用优化只能使得 ThreadLocal 被正确回收,但是这些 key 为 null 的 Entry 们仍然会存在在 ThreadLocalMap 里,因此 value 仍然无法被回收。
所以 Java 又做了一个优化,就是在 ThreadLocal 执行
get()
、set()
、remove()
方法时,都会将该线程 ThreadLocalMap 里所有 key = null 的 value 也设置为 null,手动帮助 GC:ThreadLocal k = e.get(); if (k == null) { e.value = null; // Help the GC }
但是根本上的解决办法,还是在当前线程使用完这个 ThreadLocal 时,就及时
remove()
掉该 value,也就是使得 ThreadLocalMap 中不要存在这个键值对,这样才能确保 GC 能正确回收。 -
具体实例
-
每个线程都可以在 ThreadLocal 中放自己的值,且不会干扰到其他线程的值
class Tools { public static ThreadLocal threadLocal = new ThreadLocal(); } class MyThread extends Thread { @Override public void run() { if (Tools.threadLocal.get() == null) { Tools.threadLocal.set(Thread.currentThread().getName() + ", " + Math.random()); } System.out.println(Tools.threadLocal.get()); } } public class Main { public static void main(String[] args) { for (int i = 0; i < 5; i++) { MyThread thread = new MyThread(); thread.setName("thread " + i); thread.start(); } } }
thread 1, 0.86 thread 0, 0.42 thread 2, 0.35 thread 3, 0.41 thread 4, 0.45
可以看到,不同线程的 ThreadLocalMap 中的 key 可能指向同一个 ThreadLocal 对象,也就是 ThreadLocal 可以是多线程共享的,但是 key 对应的 value 因为保存在每个线程单独的 ThreadLocalMap 中,而无法多线程共享。
注意:这里仅仅是为了演示而使用 public static 修饰 ThreadLocal,这会产生一个指向 ThreadLocal 的强引用,可能导致内存泄漏。
-
使用 ThreadLocal 在 SimpleDateFormat 上,并且给 ThreadLocal 加上泛型,指定 value 的类型是 SimpleDateFormat
因为使用了 ThreadLocal 确保每个线程有自己一份 SimpleDateFormat,所以线程安全,不会报错。
class MyThread extends Thread { private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(); @Override public void run() { SimpleDateFormat sdf = threadLocal.get(); if (sdf == null) { sdf = new SimpleDateFormat("yyyy-MM-dd"); threadLocal.set(sdf); } try { System.out.println(sdf.parse("2018-07-15")); } catch (ParseException e) { System.out.println("报錯了"); } } } public class Main { public static void main(String[] args) { for (int i = 0; i < 5; i++) { MyThread thread = new MyThread(); thread.setName("thread " + i); thread.start(); } } }
Sun Jul 15 00:00:00 CST 2018 Sun Jul 15 00:00:00 CST 2018 Sun Jul 15 00:00:00 CST 2018 Sun Jul 15 00:00:00 CST 2018 Sun Jul 15 00:00:00 CST 2018
另:注意到示例中使用 private static 修饰 ThreadLocal,这通常是受鼓励的做法,private 是为了封装,static 是为了避免重复创建 TSO(Thread Specific Object,即与线程相关的变量)。没有被 static 修饰的 ThreadLocal 变量实例,会随着所在的类多次创建而被多次实例化,这样频繁地创建变量实例没有必要。
-
使用 ThreadLocal 在 SpringMVC 上
-
拦截器 MyInterceptor 先去从 cookie 中取得当前用户信息,透过 UserUtils 放到
ThreadLocal<User>
里 -
然后当 MyController 要去取得这个请求(也就是这条线程)的用户信息时,就去调用 UserUtils 取得放在
ThreadLocal<User>
里面的 User 信息 -
最后当请求结束时,删除此条线程的
ThreadLocal<User>
信息,避免内存泄漏//UserUtils 专门存取 User 信息 public class UserUtils { public static ThreadLocal<User> userThreadLocal = new ThreadLocal<>(); public static void setUser(User user) { userThreadLocal.set(user); } public static User getUser() { return userThreadLocal.get(); } public static void removeUser() { if (userThreadLocal.get() != null) { userThreadLocal.remove(); } } } //拦截器取得 cookie 中的 User 信息,并调用 UserUtils 放到 ThreadLocal 里 //请求结束时要记得把 ThreadLocal 中的 User 刪除,因为这条线程之后还要去服务其他请求 public class MyInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle() throws Exception { User user = getUserFromCookie(); UserUtils.setUser(user); return true; } @Override public void postHandle() throws Exception { UserUtils.removeUser(); } } //MyController 调用 UserUtils 取得 ThreadLocal<User> 中的 User @Controller @RequestMapping("/") public class MyController { @RequestMapping("/") public void test() { User user = UserUtils.getUser(); System.out.println("User id: " + user.id); } }
-
-
参考:
ThreadLocal 的 Entry 为什么要继承 WeakReference?
注意:从 API 上看,似乎是将变量值保存在了 ThreadLocal 中,但是从源码上看,变量值实际上保存在了 ThreadLocalMap 中或者说 Thread 中。下面看到类似于“在 ThreadLocal 中放值”这样的说法时,应该意识到在源码层面上的另一种解释。 ↩︎