【並發編程】ThreadLocal其實很簡單



本博客系列是學習並發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。

並發編程系列博客傳送門


什么是ThreadLocal

ThreadLocal有點類似於Map類型的數據變量。ThreadLocal類型的變量每個線程都有自己的一個副本,某個線程對這個變量的修改不會影響其他線程副本的值,可以說ThreadLocal為我們提供了一個保證線程安全的新思路。需要注意的是一個ThreadLocal變量,其中只能set一個值。

ThreadLocal<String> localName = new ThreadLocal();
localName.set("name1");
String name = localName.get();

在線程1中初始化了一個ThreadLocal對象localName,並通過set方法,保存了一個值,同時在線程1中通過 localName.get()可以拿到之前設置的值,但是如果在線程2中,拿到的將是一個null。

下面來看下ThreadLocal的源碼:

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

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可以發現,每個線程中都有一個 ThreadLocalMap數據結構,當執行set方法時,其值是保存在當前線程的 threadLocals變量中,當執行get方法中,是從當前線程的 threadLocals變量獲取。 (ThreadLocalMap的key值是ThreadLocal類型)

所以在線程1中set的值,對線程2來說是摸不到的,而且在線程2中重新set的話,也不會影響到線程1中的值,保證了線程之間不會相互干擾。

上面提到ThreadLoal的變量都是存儲在ThreadLoalMap的變量中,下面給出下Thread、ThreadLoal和ThreadLoalMap的關系。

Thread類有屬性變量threadLocals (類型是ThreadLocal.ThreadLocalMap),也就是說每個線程有一個自己的ThreadLocalMap ,所以每個線程往這個ThreadLocal中讀寫隔離的,並且是互相不會影響的。一個ThreadLocal只能存儲一個Object對象,如果需要存儲多個Object對象那么就需要多個ThreadLocal!

ThreadLocal使用場景

說完ThreadLocal的原理,我們來看看ThreadLocal的使用場景。

1. 保存線程上下文信息,在任意需要的地方可以獲取
比如我們在使用Spring MVC時,想要在Service層使用HttpServletRequest。一種方式就是在Controller層將這個變量傳給Service層,但是這種寫法不夠優雅。Spring早就幫我們想到了這種情況,而且提供了現成的工具類:

public static final HttpServletRequest getRequest(){
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    return  request;
}

public static final HttpServletResponse getResponse(){
    HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
    return response;
}

上面的代碼就是使用ThreadLocal實現變量在線程各處傳遞的。

2. 保證某些情況下的線程安全,提升性能
性能監控,如記錄一下請求的處理時間,得到一些慢請求(如處理時間超過500毫秒),從而進行性能改進。這邊我們以Spring MVC的攔截器功能為列子。


public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {  
    //NamedThreadLocal是Spring對ThreadLocal的封裝,原理一樣
    //在多線程情況下,startTimeThreadLocal變量必須每個線程之間隔離
    private NamedThreadLocal<Long>  startTimeThreadLocal = new NamedThreadLocal<Long>("StopWatch-StartTime");  
    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {  
        //1、開始時間
        long beginTime = System.currentTimeMillis();  
        //線程綁定變量(該數據只有當前請求的線程可見)  
        startTimeThreadLocal.set(beginTime);
        //繼續流程  
        return true;
    }  
      
    @Override  
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) throws Exception {  
        long endTime = System.currentTimeMillis();//2、結束時間  
        long beginTime = startTimeThreadLocal.get();//得到線程綁定的局部變量(開始時間)  
        long consumeTime = endTime - beginTime;//3、消耗的時間  
        if(consumeTime > 500) {//此處認為處理時間超過500毫秒的請求為慢請求  
        //TODO 記錄到日志文件  
        System.out.println(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));  
        }          
    }  
}  

說明:其實要實現上面的功能,完全可以不用ThreadLocal(同步鎖等),但是上面的代碼的確是說明ThreadLocal這個是用場景很好的列子。

ThreadLocal的最佳實踐

從上面的圖中可以看到,Entry的key指向ThreadLocal用虛線表示弱引用 ,下面我們來看看ThreadLocalMap:

java對象的引用包括 : 強引用,軟引用,弱引用,虛引用 。

弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,無論內存是否充足,該對象僅僅被弱引用關聯,那么就會被回收。當僅僅只有ThreadLocalMap中的Entry的key指向ThreadLocal的時候,ThreadLocal會進行回收的!!!
ThreadLocal被垃圾回收后,在ThreadLocalMap里對應的Entry的鍵值會變成null,但是Entry是強引用,那么Entry里面存儲的Object,並沒有辦法進行回收,所以ThreadLocalMap 存在內存泄露的風險。

所以最佳實踐,應該在我們不使用的時候,主動調用remove方法進行清理。這里給出一個建議方案:


public class Dynamicxx {
    
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public void dosomething(){
        try {
             contextHolder.set("name");
            // 其它業務邏輯
        } finally {
            contextHolder .remove();
        }
    }

}

簡單總結

  • 每個Thread對象內部都有一個ThreadLoacalMap的成員變量,這個變量類似一個Map類型,其中key為我們定義的ThreadLocal變量的this引用,value則為我們使用set方法設置的值;
  • 如果線程不消亡,在ThreadLocalMap中存放的ThreadLocal實例對象可能一直不會清除,所以當我們不需要在使用ThreadLocal的值時,就應該手動調用remove方法清除該值。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM