線程利器: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>

總結

醉裏且貪歡笑,要愁那得工夫。近來始覺古人書,信著全無是處。 昨夜鬆邊醉倒,問鬆我醉何如。只疑鬆動要來扶,以手推鬆曰去! ——辛棄疾《西江月·遣興》
相關文章
相關標籤/搜索