线程利器:ThreadLocal

引言

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

@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不执行。spring

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

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

解决

分析

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

由于并发请求时,每一个请求分配一个线程管理Socket数据结构

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

image.png

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

ThreadLocal

ThreadLocal是啥?去看看《Java编程的逻辑》吧!post

咱们想用一个相似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();

运行结果以下:

image.png

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

image.png

接着看:获取到了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(可继承的本地策略)。

image.png

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

总结

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