ThreadLocal变量可以理解为线程内部私有的共享变量。在Java的内存模型中每个线程拥有自己的工作线程,这个变量是一个map结构的专门用于线程私有变量的存储,方便于解耦+防止并发。StackOverFlow上有段解释较为通俗。When and how can we use ThreadLocal variable?:
- When an object is not thread-safe, instead of synchronization which hampers the scalability, give one object to every thread and keep it thread scope, which is ThreadLocal. One of most often used but not thread-safe objects are database Connection and JMSConnection.
- One example is Spring framework uses ThreadLocal heavily for managing transactions behind the scenes by keeping these connection objects in ThreadLocal variables. At high level, when a transaction is started it gets the connection ( and disables the auto commit ) and keeps it in ThreadLocal. on further db calls it uses same connection to communicate with db. At the end, it takes the connection from ThreadLocal and commits ( or rollback ) the transaction and releases the connection.I think log4j also uses ThreadLocal for maintaining MDC.
通常线程池这种并发的资源模型会面临多线程竞争,可以将线程的每个连接初始化到每个线程中避免竞争。这时候会将这个连接放置到ThreadLocal变量中。
ThreadLocal API
ThreadLocal变量实际上是以当前线程为key,可以指定一个value的map对象,只能容放一个kv键值对。
1 | package com.souche.study.TreadLocal; |
结果:
1 | get初始化ThreadLocal:1 |
Storing User Data in a Map
我们假定每个线程都需要存储它的登录信息,这里给定一个用户信息类:
1 | public class Context { |
我们要实现每个userId对应一个context就需要将这个userId作为key存储在map中,同时需要时concurrentHashMap满足多线程并发的线程安全。
1 | public class SharedMapWithUserContext implements Runnable { |
然后我们通过测试类去测试:
1 | SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1); |
这种方式也是可以的,相对来说需要去做一个线程安全的东西成本比较大。
Storing User Data in ThreadLocal
向上面这种情况我们可以将这个context存在每个线程自有的ThreadLocal变量中,不用考虑线程安全的问题:
1 | public class ThreadLocalWithUserContext implements Runnable { |
同样的,我们可以进行测试:
1 | ThreadLocalWithUserContext firstUser |
可以得到一个结果:
1 | thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'} |
ThreadLocal如何实现
很多blog都介绍了ThreadLocal主要作用是解耦+防止并发。实际上我自己将其理解为一个全局map变量,全局变量的存在减少了不必要的参数传递,也就是解耦,这个全局变量我们一般通过定义变量为private static实现,需要通过类直接访问可以定义为public static。通过维护当前线程id作为map的key,保证了只有当前key的value对当前线程可见,其他线程无法访问到这个value的资源,那么这个value便可以理解为线程的私有资源,实际上这些对象都还是放在堆中,只不过用key标示了每个资源的归属 ,也就是每个工作线程都有自己的私有空间。
1 | /** |
一般我们的框架在处理了登录之后,例如单点登录,一个请求进入到tomcat,分配线程资源,进入到后台,通过拦截器分发的对应的sso服务。服务返回登录信息,这个时候会写一个ThreadLocal变量(AuthNHolder)以当前线程id为key,保存用户信息到value里面,之后线程在任何地方想获取这个用户信息可以直接从ThreadLocal中拿到,线程请求完成之后回到拦截器会将这个value清空。这里应该可以体会到解耦+防止并发。
总结
其实我们在使用这个变量的时候,通常也要防止在线程池的情况下,可能会导致某一个线程同时消费很多任务,如果在一个任务结束之后没有将线程的工作空间进行清除,那么这个工作空间会存储两个任务的信息,这可能会产生问题。我们可以通过登录保存用户信息这个案例更好的了解ThreadLocal的应用。详细可以参考optimus中AuthHolder的实现。