乐趣区

线程利器ThreadLocal

引言

这是 JWT 认证条件下的 getCurrentLoginUser 代码实现,请分析性能:

@Override
@ApiOperation("获取当前登录的用户")
public User getCurrentLoginUser() {if (this.currentLoginUser != null) {return this.currentLoginUser;} else {
        // 获取认证数据并查询登陆用户
        Claims claims = JwtUtils.getClaims(this.getHttpServletRequest());
        Long userId = JwtUtils.getUserId(claims);
        return this.getAuthInterceptor().getUserById(userId);
    }
}

在生产环境中,currentLoginUser永远为 nullif 不执行。

执行 else 内的解析 JWT 的代码,解析userId,再查询用户。

很明显,一次请求内,当 getCurrentLoginUser 被多次调用时,会重复解析JWT,就会产生性能问题。

解决

分析

解决重复解析 JWT 的唯一思路就是缓存,第一次解析完 userId 并查询出 user 后将这个 user 对象缓存。

因为并发请求时,每个请求分配一个线程管理Socket

所以当前待解决的问题就变成了:如何设计一种缓存,使之各线程不影响,线程安全。

简单的设计如上,一个 MapThread 作为key,用户缓存作为value

ThreadLocal

ThreadLocal是啥?没听说过是不是?其实这是 Java 里最基础的东西。我的 Java 到底学了个啥呀?

日常吐槽,我发现其他学校竟然讲 spring-bootspring-cloudConcurrentHashMap 源码,人家上完专业课直接精通kafka

一般人知道 HashMap 的阀值为什么是 8 吗?

对不起,这些人家老师都讲过。而《河北工业大学》的“大博士”,你讲个检查异常都讲错了,被学生指出之后还不改。你去学学 Java 再来讲课好吗?

最后的结果就是,我们辛辛苦苦准备了好几个月的东西,人家课上就精通完了。怪不得干不过人家,我们引以为傲的 spring-cloudRPC 原来都属于课上的基础知识。

继续。

我们想用一个类似 Map<Thread, User> 这样的数据结构来设计缓存,其实 JDK 中早就为我们封装好了,即ThreadLocal<T>

栗子

大家来看下面的示例代码:

ThreadLocal<String> local = new ThreadLocal<>();
Thread thread1 = new Thread(() -> {local.set("test1");
    System.out.println("线程 1 set 完毕");
    try {Thread.sleep(100);
    } catch (InterruptedException e) { }
    System.out.println("线程 1:" + local.get());
});
Thread thread2 = new Thread(() -> {local.set("test2");
    System.out.println("线程 2 set 完毕");
    try {Thread.sleep(100);
    } catch (InterruptedException e) { }
    System.out.println("线程 2:" + local.get());
});
thread1.start();
thread2.start();

运行结果如下:

main线程创建 ThreadLocal 对象,thread1thread2操作的是同一个对象 localthread1thread2 分别 set 数据,两线程再次从 local 中获取数据的时候,能够保证两者数据不冲突。

ThreadLocal<String> local = new ThreadLocal<>();

底层原理

ThreadLocal中的 set 方法实现如下:

获取当前线程,同时通过线程对象获取ThreadLocalMap

public void set(T value) {Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

set内调用了 getMap 方法,看看 getMap 的内部实现:

返回线程对象内的 threadLocals 属性。

ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

Thread类中的成员属性threadLocals,默认为null

接着看:获取到了 Map 之后,用当前的 ThreadLocal 对象作为 key 存储 valueMap

if (map != null)
    map.set(this, value);
else
    createMap(t, value);

这样设计十分地精妙,每一个线程都有独立的 Map 存储,肯定能做到数据隔离且安全。且可方便地创建多个安全的 ThreadLocal 进行存储。

改写

鉴于 ThreadLocal 的特性,我们可以设计一个 SecurityContext 以封装ThreadLocal<User>

@Component
public class SecurityContext {private ThreadLocal<User> local = new ThreadLocal<>();

    public void set(User user) {local.set(user);
    }

    public User get() {return local.get();
    }

    public void clear() {local.remove();
    }
}

写一个拦截器,思路如下:

pre 里解析 JWT,并存到SecurityContext 里。

postclear,防止线程池线程复用导致数据错误。

然后原来的 getCurrentLoginUser 方法直接从 SecurityContextget即可。

熟悉吗?

看到 SecurityContext 这个名称是不是很熟悉?

spring-security 中获取用户信息的方法如下,它怎么实现的呢?

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

点开 spring-security 源码,有三种策略:GlobalSecurityContextHolderStrategy(即全局线程共享策略)、ThreadLocalSecurityContextHolderStrategy(本地策略)、InheritableThreadLocalSecurityContextHolderStrategy(可继承的本地策略)。

点开源码后发现,其实 spring-security 就是这么简单,内部也是用 ThreadLocal 实现的,只是此处存储的信息比较多,使用ThreadLocal<SecurityContext>

写框架的也是程序员,只是他们的基础比我们好而已。

总结

醉里且贪欢笑,要愁那得工夫。近来始觉古人书,信著全无是处。
昨夜松边醉倒,问松我醉何如。只疑松动要来扶,以手推松曰去!
——辛弃疾《西江月·遣兴》

教育什么时候能改革呀?我太渺小了,还有人听得见吗?

退出移动版