介绍
ThreadLocal 是 JDK底层提供的一个解决多线程并发问题的工具类,它为每个线程提供了一个本地的副本变量机制,实现了和其它线程隔离,并且这种变量只在本线程的生命周期内起作用,可以减少同一个线程内多个方法之间的公共变量传递的复杂度。
举一个形象的例子:比如,让100个人填写个人信息表,如果只有一支笔,那么每个人只能排队依次来拿笔填写,那么管理员就必须保证大家不会去哄抢这仅存的一支笔,否则谁也填不完。但如果从另一个角度思考,如果我们准备100支笔,每人一支笔,所有人都可以各自为营,很快的填完表格完成工作。在这个例子中,第一种就是锁的一种思路,人手一支笔就是ThreadLocal实现的第二种思路。
应用场景
ThreadLocal主要解决2类问题:
1、并发问题:使用ThreadLocal代替Synchronized来保证线程安全,同步机制采用空间换时间 -> 仅仅先提供一份变量,各个线程轮流访问,后者每个线程都持有一份变量,访问时互不影响。
2、数据存储问题:ThreadLocal为变量在每个线程中创建了一个副本,所以每个线程可以访问自己内部的副本变量。
SimpleDateformat对象线程安全使用场景举例
/**
* SimpleDateFormat 线程不安全的使用
*/
public class NoUseThreadLocalDemo {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
Date t = sdf.parse("2020-06-13 20:00:"+i%60);
System.out.println(i+":"+t);
}catch (ParseException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i=0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
}
上述代码在多线程环境中,你很有可能会得到下面的异常
Exception in thread "pool-1-thread-11" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.example.demo.thread.threadLocal.NoUseThreadLocalDemo$ParseDate.run(NoUseThreadLocalDemo.java:24)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
出现这些问题的原因,是SimpleDateFormat.parse()方法并不是线程安全的。因此,在线程池中共享这个对象必然导致错误。
一种可行的方案是在sdf.parse()前后加锁
synchronized(obj){
Date t = sdf.parse("2020-06-13 20:00:"+i%60);
}
当然,这里我们可以采用人手一支笔的思路,使用ThreadLocal为每一个线程都产生一个SimpleDateFormat对象实例
/**
* SimpleDateFormat 使用ThreadLocal创建线程的副本,保证线程安全
*/
public class UseThreadLocalDemo {
static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<>();
public static class ParseDate implements Runnable{
int i=0;
public ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
// 如果当前线程不持有SimpleDateFormat对象实例,创建一个并设置到当前线程
if (t1.get()==null){
t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
Date t = t1.get().parse("2020-06-13 20:00:"+i%60);
System.out.println(i+":"+t);
}catch (ParseException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i=0;i<1000;i++){
es.execute(new ParseDate(i));
}
}
}
注意:为每一个线程分配不同的对象,需要在应用层面保证。ThreadLocal只是起到了简单的容器作用。
ThreadLocal在Spring中的应用场景
ThreadLocal还有一种场景是在API层,我们经常需要request这个参数,我们可能就需要在很多场景下使用这个参数,但是每个方法都把它作为参数的话会让方法的参数过多不好维护,所以我们可以把这些request都对应到一个线程上面,一个线程内如果想使用这个参数,直接去取就行了。
简而言之就是每个线程拥有自己的实例,然后实例需要在对应线程的使用的多个方法中共享但是不希望被多线程共享。
一般情况下,只有无状态的Bean才会在各个实例中共享,在Spring中绝大多数的Bean都可以声明为 singleton单例的,比如一些request相关的 非线程安全状态采用了ThreadLocal让它们成为线程安全的状态。一般情况下,web应用划分成MVC三层,在不同的层次中编写对应的逻辑,下层通过接口向上层开放功能调用,正常情况下,从接收请求到响应都应该属于同一个线程。而ThreadLocal是一个很好的机制,它为每个线程提供了一个独立的变量副本解决了变量并发访问的冲突问题,比 Synchronized要简单且方便,可以让程序具备更高的并发性.
以HttpRequest为例说明项目中如何使用ThreadLocal的代码
// 定义一个全局的Filter
public class CommonFilter extends OncePerRequestFilter {
/**
* 拦截所有的http请求,需要配置过滤器
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 把request塞到线程的ThreadLocal中
RequestManager.setHttpServletRequest(request);
filterChain.doFilter(request, response);
} finally {
RequestManager.removeHttpServletRequest();
}
}
}
// http请求管理
public class RequestManager {
private static ThreadLocal<HttpServletRequest> threadLocal = new ThreadLocal<HttpServletRequest>();
/**
* 当前线程加入request
* @param request
*/
public static void setHttpServletRequest(HttpServletRequest request){
if(request != null){
threadLocal.set(request);
}
}
/**
* 当前线程获取request,在API接口中可以直接调用这个方法获取当前线程的request对象
*/
public static HttpServletRequest getHttpServletRequest(){
return threadLocal.get();
}
/**
* 清理request,释放空间
*/
public static void removeHttpServletRequest(){
threadLocal.remove();
}
}
// Test
@RestController
public class Demo {
@RequestMapping("/testDemo")
public void test(String s){
/**
* 通过这种方式就可以把请求取出来了,不用每次都在参数上加一个request了
*/
HttpServletRequest request = RequestManager.getHttpServletRequest();
}
}
ThreadLocal 的实现原理
Thread、ThreadLocal、ThreadLocalMap之间的关系。
概括一下:ThreadLocal并不是把Thread作为key,副本值作为value的一种类似HashMap的结构。而是每个Thread里都有一个ThreadLocalMap,ThreadLocal只是操作每个线程的 ThreadLocalMap而已。
每个线程都维护一个ThreadlocalMap哈希表(类似HashMap),这个哈希表的key是ThreadLocal对象本身,value是要存储的局部副本值,这样的话存储数量是ThreadLocal的数量决定的。当Thread销毁之后,ThreadLocalMap也会被随之销毁,减少内存占用。
而ThreadLocalMap的实现原理跟HashMap差不多,内部有一个Entry数组,一个Entry通常至少包括key,value,特殊的是这个Entry继承了WeakReference也就是说它是弱引用的所以可能会有 内存泄露的情况。ThreadLocal负责管理ThreadLocalMap,包括插入,删除等等.另一方面来说ThreadLocal基本上就相当于门面设计模式中的一个Facade类。key就是ThreadLocal对象自己,同时,很重要的一点:ThreadLocal把Map存储在当前线程对象里面。
ThreadLocal成员属性
public class ThreadLocal<T> {
/**
* 自定义哈希码(ThreadLocalMaps的),降低哈希冲突
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 生成下一个哈希码的hashCode,操作是原子的,从0开始
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 连续分配的两个ThreadLocal实例的threadLocalHashCode值的增量
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一个哈希码的hashCode,此方法是一个原子类不停地去加上斐波那契散列数,使得哈希值分布均匀
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// ......
// ...
}
ThreadLocal的主要方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程维护的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果存在map,将当前ThreadLocal对象作为key保存
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal则通过 {@link #initialValue}方法进行初始化值
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// this指当前ThreaLocal对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 不存在就返回一个初始化的值
return setInitialValue();
}
/**
* 此方法第一次调用发生在当线程通过 {@link #get} 方法访问此线程的ThreadLocal值时
* 除非线程先调用了 {@link #set},在这种情况下,{@code initialValue} 方法才不会被这个线程调用
* 通常情况下,每个线程最多调用1次这个方法,但是也可能再次调用,比如 {@link #remove} 被调用后,调用get
* 这个方法仅仅简单返回null{@code null}; 如果程序想要它返回除了null之外的初始值,必须继承重写此方法,
* 通常使用匿名内部类的方式实现
* @return 返回当前ThreadLocal的初始值 null
*/
protected T initialValue() {
return null;
}
/**
* set的变样实现,用于初始化值initialValue
* 用来代替防止用户重写set而无法初始化
* @return 返回初始化后的值
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果此map村咋洗,调用map,set设置此实体entry
if (map != null)
map.set(this, value);
else
// map不存在时,调用此方法进行ThreadLocalMap对象初始化并将此entry作为第一个值放进去
createMap(t, value);
// 返回设置的value值s
return value;
}
/**
* 删除当前线程中保存ThreadLocal对应的实体entry
* 如果此ThreadLocal变量在当前线程中调用{@linkplain #get read} 方法
* 则会通过调用{@link #initialValue} 方法进行初始化
* 除非此值value是通过当前线程内置调用set方法设置
* 这可能导致在当前线程中多次调用initialValue方法初始化
* 1. 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
* 2. 判断`ThreadLocalMap`是否存在,如果存在,调用map.remove,以当前的`ThreadLocal`为key删除对应的`entry`
*/
public void remove() {
// 获取当前线程Thread对象,进而获取此线程对象中维护的`ThreadLocalMap`对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 如果`ThreadLocalMap`存在调用remove方法删除之,当前ThreadLocal对象为key
m.remove(this);
}
/**
* 获取当前对象Thread对应维护的ThreadLocalMap
* @param 当前线程
* @return 对应的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
注意:ThreadLocalMap定义在Thread中,并由线程来维护自己的副本
public class Thread implements Runnable{
/**
* 由线程自己来维护ThreadLocalMap
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/**
* InheritableThreadLocal主要用于子线程创建时,
* 需要自动继承父线程的ThreadLocal变量,方便必要信息的进一步传递。
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// .....
// ...
}
当线程退出时,Thread类会进行一些清理工作
/**
* Thread类中的方法,当线程退出前,由系统回调,进行资源清理
*/
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
// 加速资源清理
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
但是,当我们使用线程池的时候,当前线程未必会退出。如果将太大的对象设置到ThreadLocal中,可能会使系统出现内存泄露的可能(这里的意思是:你设置了对象到ThreadLocal中,但是不清理它,当你使用几次后,不会再有用了,但它却无法被回收)
解决方法:
1. ThreadLocal.remove()方法将变量移除
2. 手动将其设置为null,比如tl=null
参考资料
《Java高并发程序设计》 葛一鸣 郭超 著