一、关于
ThreadLocal不是一个线程,而是一个线程的本地化对象,当某个变量在使用ThreadLocal维护时,它会在每个线程中创建独立的一个副本,不同线程中对这个变量的修改不会影响到其他线程中这个变量的值。
ThreadLocal采用了空间换时间的思想,主要用来实现多线程环境下线程安全和保存线程上下文中的变量。
可以笼统的理解它为线程范围下的全局变量存储对象。
二、API
- public void set(T value) {}
将T类型的值设置到ThreadLocal中 - public T get() {}
获取ThreadLocal对象的值 - public void remove() {}
移除ThreadLocal中的值
三、源码分析
1.1set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 首先获取到当前线程。
- 根据getMap方法根据当前线程获取到ThreadLocalMap 。我们看一下getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
直接返回的是t.threadLocals,threadLocals是Thread类的一个参数,类型是ThreadLocal中的内部类ThreadLocalMap,初始值为null。
再看ThreadLocal类结构:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...............
private Entry[] table;
...............
}
其内部维护了一个Entry数组,且注意Entry类是继承自WeakReference,弱引用对象,所以需要注意jvm回收引发的问题。
- 回到set方法
- 如果获取出来的map不为空,就将当前ThreadLocal对象和value值放进去。
- 如果map为空,就创建map
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
1.2get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
- 获取当前线程。
- 获取当前线程的所有ThreadLocal对象(ThradLocalMap)。
- 如果map不为空,就根据ThreadLocal对象获取到存储在其中的值。
- 如果map为空,则初始化将当前ThreadLocal对象中的值设为null,并放入ThreadLocalMap。
1.3remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
获取当前线程的所有ThreadLocal对象,并将当前对象从ThreadLocalMap 中移除。
这里需要着重注意,不然会引起jvm内存泄漏!
上面说到了Entry是继承WeakReference的对象就是弱引用对象,而调用构造方法时,也说明了Entry的key是指向当前ThreadLocal的弱引用对象。
弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
如果Entry的key被回收了,那么其对应的值就不会被remove掉了。所以在使用完之后需要手动remove()。
四、使用示例
这里我们来模拟将用户登录信息存放到ThreadLocal中,项目为SpringBoot项目,结构为:
缓存工具类CacheUtil,此类用于提供对ThreadLocal增删查。
package com.yl.demo.util;
public class CacheUtil {
private static ThreadLocal<String> store = new ThreadLocal<String>();
public static String getStore() {
return store.get();
}
public static void setStore(String value) {
store.set(value);
}
public static void remove() {
store.remove();
}
}
拦截器TestInterceptor,拦截所有请求,在进入控制器之前,在ThreadLocal中设置信息,在执行完控制器的方法后,移除ThreadLocal中的值。
package com.yl.demo.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.yl.demo.util.CacheUtil;
public class TestInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println(Thread.currentThread().getName());
CacheUtil.setStore("这里面是用户登录信息");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
CacheUtil.remove();
}
}
mvc配置类,让拦截器拦截所有路径。
package com.yl.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.yl.demo.interceptor.TestInterceptor;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Bean
public TestInterceptor testInterceptor() {
return new TestInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(testInterceptor()).addPathPatterns("/**");
}
}
测试Controller
package com.yl.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.yl.demo.util.CacheUtil;
@RestController
public class TestController {
@GetMapping("/get")
public String doGet() {
return Thread.currentThread().getName()+"-get : " + CacheUtil.getStore();
}
}
运行项目,在浏览器测试:
可以看到,同一线程下都可以获取到ThreadLocal中的值。
五、子线程获取父线程中ThreadLocal的值
我们这里改造一下接口:
package com.yl.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.yl.demo.util.CacheUtil;
@RestController
public class TestController {
@GetMapping("/get")
public String doGet() {
// 新起一个线程获取值
new Thread(()-> {
System.out.println(Thread.currentThread().getName()+"-get : " + CacheUtil.getStore());
}).start();
return Thread.currentThread().getName()+"-get : " + CacheUtil.getStore();
}
}
new Thread(()-> {}).start()这种写法是java8中的写法,如果jdk低于1.8,就会报错,那么可以根据自己的实际情况来写,这里实现的就是新启动一个线程来获取存储在ThreadLocal中的值,看能否取到。
我们可以看到,新线程为什么取到的是null呢?
因为ThreadLocal是线程本地变量,设置值的时候是http-nio-8080-exec-1这个线程设置的,对其他线程没有影响,所以Thread-103-get线程是取不到值的。
在这个方法中,线程Thread-103-get就是线程http-nio-8080-exec-1的子线程。
那么我们如果想在子线程中获取父线程的ThreadLocal值,应该怎样去做呢?
只需要改变一个地方,那就是将ThreadLocal替换成InheritableThreadLocal:
重启项目,测试:
子线程中可以获取到父线程中ThreadLocal的值了!
六、InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的子类。
那么值究竟是从哪里获取出来的呢?
首先看一下InheritableThreadLocal类中相对于ThreadLocal的改动:
- 重写了getMap方法:
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
- 重写了createMap方法:
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
再看一下Thread的创建过程:
最终init方法(省略版):
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
.................
Thread parent = currentThread();
.................
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
.................
}
此处的currentThread()线程就是上文中的http-nio-8080-exec-1(父)线程。
此时inheritThreadLocals为true,且parent线程的inheritableThreadLocals 不为空的,所以就会将parent的inheritableThreadLocals赋给新创建的线程的inheritableThreadLocals ,也就是上文中的Thread-103-get(子)线程。所以子线程中就有了父线程中所有ThreadLocal的值。