你会用ThreadLocal嘛?



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,所以也不会达到多线程直接数据隔离的效果,太坑了!!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寂寞旅行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值