ThreadLocal介绍
意思为线程本地变量,用于解决多线程并发时访问共享变量的问题;
怎么解决呢? 原理是什么?
synchronized也能保证并发时候的数据问题,有什么区别呢?
提示:以下是本篇文章正文内容,下面案例可供参考
一、与synchronized的区别?
synchronized关键字主要解决多线程共享数据同步问题。(数据不同步,想让数据同步)
ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。(数据应该是隔离的,不想相互混淆)
保证数据安全的本质区别不同:
1 synchronized 是利用锁机制,同一时间只能有一个线程访问该变量,牺牲了时间解决访问冲突;这样多线程操作同一个变量也不会有问题了;
2 ThreadLocal 是利用副本的概念,每一个线程中都有一个变量的副本(从主内存中将该变量复制的当前线程中),线程之间的变量互不影响,牺牲了空间解决访问冲突,这样达到了线程之间数据隔离的效果;
二、使用场景
1.在分布式项目中的关键信息传递
分布式项目中, 一般都有一个单独的用户中心,鉴权模块 我们这里模拟为 u,其他模块 模拟为 a b c; 当用户在u模块登录后,访问其他模块的时候,是要带着关键信息的,比如token;
为什么呢? 因为其他模块每次接受请求都需要去u模块鉴别当前请求是否合法,如果合法,那么进入模块请求开始,这里涉及到一个隐藏问题,当鉴权成功后,我如果想获取到当前登录的用户信息,怎么做呢?
1 每个模块解析token 如果这样,就需要a b c模块从header中获取token, 哪里用到用户信息,就解析一次token,是不是很复杂? 而且token要一直传递,比如我的方法比较复杂,那么当请求进入a模块, 我的方法调用依次是 A> B > C,我就需要把token 从A传递至C;
2 u 模块解析token,这样是常规做法,相当于u模块是统一的用户鉴权模块,然后将解析信息放入header中,然后通过拦截器,将本次信息放入到ThreadLocal 中,然后就可以在每个模块中,直接获取了!!! 用完记得移除掉,一般也在拦截器中处理,当然也可以自行处理;
2.使用示例
代码如下(示例):
@Slf4j
@Component
public class AuthInfoInterceptor implements HandlerInterceptor {
@Autowired
AuthUserService authUserService;
@Value("${sa-token.timeout}")
private long timeout;// 单位s
@Autowired
AuthCompanyDao authCompanyDao;
@Autowired
AuthUserRoleDao authUserRoleDao;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String tenantId = request.getHeader(TenantEnum.TENANT_ID.getName());
log.info(">>>>>>>拦截到api相关请求头<<<<<<<<" + tenantId);
if (!StringUtils.isEmpty(tenantId)) {
// 项目是否存在
AuthCompany authCompany = authCompanyDao.queryById(Long.valueOf(tenantId));
if (ObjectUtil.isNull(authCompany)) {
throw new GraphExecuteException("项目不存在");
}
// 人和项目是否一致
Integer roleId = authUserRoleDao.queryRoleId(StpUtil.getLoginIdAsInt(), Long.parseLong(tenantId));
if (null == roleId) {
throw new GraphExecuteException("当前用户不属于该项目");
}
//直接搂下来,放到ThreadLocal中 后续直接从中获取
CurrentLocal.set(TenantEnum.SYSTEM_TENANT_ID.getName(), tenantId);
}
log.info("token剩余过期时间:{}", StpUtil.getTokenTimeout());
log.info("临时token剩余过期时间:{}", StpUtil.getTokenActivityTimeout());
// 续约token
if (StpUtil.getTokenTimeout() <= StpUtil.getTokenActivityTimeout()) {
StpUtil.renewTimeout(timeout);
log.info("token时间重置:{}", StpUtil.getTokenTimeout());
}
return true;//注意 这里必须是true否则请求将就此终止。
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除租户信息
CurrentLocal.remove();
}
}
ThreadLocal工具类
@Slf4j
public class CurrentLocal {
private static final ThreadLocal<Map<String,Object>> CURRENT_LOCAL = ThreadLocal.withInitial(HashMap::new);
// 多线程变量副本
public static void remove() {
CURRENT_LOCAL.remove();
}
public static void set(String name,Object tenantId) {
CURRENT_LOCAL.get().put(name,tenantId);
log.info("设置成功: {}", JsonUtil.toJson(CURRENT_LOCAL.get()));
}
public static Long getCompanyId() {
if (CollUtil.isEmpty(CURRENT_LOCAL.get())) {
return null;
}
return Long.valueOf(CURRENT_LOCAL.get().get(TenantEnum.SYSTEM_TENANT_ID.getName()).toString());
}
public static Object get(String key) {
if (CollUtil.isEmpty(CURRENT_LOCAL.get())) {
return null;
}
return CURRENT_LOCAL.get().get(key);
}
}
3.验证ThreadLocal多线程直接互不影响
public class CurrentLocalTest {
public static void main(String[] args) {
CurrentLocal.set("a","123");
System.out.println(CurrentLocal.get("a")+"====="+Thread.currentThread().getName());
new Thread(() -> {
CurrentLocal.remove();
Thread.currentThread().setName("remove");
System.out.println(CurrentLocal.get("a")+"===="+Thread.currentThread().getName());
}).start();
new Thread(() -> {
Thread.currentThread().setName("set");
CurrentLocal.set("a","嘿嘿");
System.out.println(CurrentLocal.get("a")+"====="+Thread.currentThread().getName());
}).start();
System.out.println(CurrentLocal.get("a")+"====="+Thread.currentThread().getName());
}
}
4. 弱引用
- 我们平时用的是ThreadLocal ,但是其实真正起作用的是 ThreadLocalMap 它才是真正存储数据的;
- 是的,ThreadLocalMap 也是个map 它的key,其实就是 ThreadLocal ;
- 因为 ThreadLocal 是一种空间换时间的做法,所以每个线程都会有一个副本,自己的"共享资源",所以是唯一的;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
如上部分 ThreadLocalMap 源码,可以发现:
1 ThreadLocalMap 内部使用的Entry
2 Entry extends WeakReference<ThreadLocal<?>> 是一个弱引用
- 弱引用? 有什么作用?
如果threadLocal外部强引用被置为null(threadLocalInstance=null)的话,threadLocal实例就没有一条引用链路可达,很显然在gc(垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。
如果线程执行结束后,threadLocal,threadRef会断掉,因此threadLocal,threadLocalMap,entry都会被回收掉;
ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。试想如果使用强引用,等于ThreadLocalMap中的所有数据都是与Thread的生命周期绑定,这样很容易出现因为大量线程持续活跃导致的内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。
总结
开始的时候在网上找了几个工具类,确实有坑,昨天研究了几个小时终于弄懂了,然后也解决了项目中的问题,不过开始的时候那个工具类真的让我怀疑人生了,因为数据直接相互影响,记住千万不用像这篇博客这样用 错误用法 ThreadLocal
它的错误在于
private static Map<Object, Object> cacheMap;
static {
cacheMap = MAP_THREAD_LOCAL.get();
}
然后每次使用,用的都是cacheMap,这其实就相当于在用一个静态的map存储,而没有用ThreadLocal,所以也不会达到多线程直接数据隔离的效果,太坑了!!