線程本地存儲模式:ThreadLocal

話題:線程本地存儲模式:沒有共享,就沒有傷害

線程封閉,其本質上就是避免共享。你已經知道通過局部變量可以做到避免共享,那還有沒有其他方法可以做到呢?有的,Java語言提供的線程本地存儲(ThreadLocal)就能夠做到。下面我們先看看ThreadLocal到底該如何使用。

ThreadLocal的使用方法

下面這個靜態類ThreadId會爲每個線程分配一個唯一的線程Id,如果一個線程前後兩次調用ThreadId的get()方法,兩次get()方法的返回值是相同的。但如果是兩個線程分別調用ThreadId的get()方法,那麼兩個線程看到的get()方法的返回值是不同的。若你是初次接觸ThreadLocal,可能會覺得奇怪,爲什麼相同線程調用get()方法結果就相同,而不同線程調用get()方法結果就不同呢?

static class ThreadId {
  static final AtomicLong nextId=new AtomicLong(0);
  
  //定義ThreadLocal變量
  static final ThreadLocal<Long> tl=ThreadLocal.withInitial(
    ()->nextId.getAndIncrement());
    
  //此方法會爲每個線程分配一個唯一的Id
  static long get(){
    return tl.get();
  }
}

能有這個奇怪的結果,都是ThreadLocal的傑作,不過在詳細解釋ThreadLocal的工作原理之前,我們再看一個實際工作中可能遇到的例子來加深一下對ThreadLocal的理解。你可能知道SimpleDateFormat不是線程安全的,那如果需要在併發場景下使用它,你該怎麼辦呢?

其實有一個辦法就是用ThreadLocal來解決,下面的示例代碼就是ThreadLocal解決方案的具體實現,這段代碼與前面ThreadId的代碼高度相似,同樣地,不同線程調用SafeDateFormat的get()方法將返回不同的SimpleDateFormat對象實例,由於不同線程並不共享SimpleDateFormat,所以就像局部變量一樣,是線程安全的。

static class SafeDateFormat {
  //定義ThreadLocal變量
  static final ThreadLocal<DateFormat> tl=ThreadLocal.withInitial(
    ()-> new SimpleDateFormat(
      "yyyy-MM-dd HH:mm:ss"));
      
  static DateFormat get(){
    return tl.get();
  }
}
//不同線程執行下面代碼
//返回的df是不同的
DateFormat df = SafeDateFormat.get()

通過上面兩個例子,相信你對ThreadLocal的用法以及應用場景都瞭解了,下面我們就來詳細解釋ThreadLocal的工作原理。

ThreadLocal的工作原理

在解釋ThreadLocal的工作原理之前, 你先自己想想:如果讓你來實現ThreadLocal的功能,你會怎麼設計呢?ThreadLocal的目標是讓不同的線程有不同的變量V,那最直接的方法就是創建一個Map,它的Key是線程,Value是每個線程擁有的變量V,ThreadLocal內部持有這樣的一個Map就可以了。你可以參考下面的示意圖和示例代碼來理解。
在這裏插入圖片描述

class MyThreadLocal<T> {
  Map<Thread, T> locals = new ConcurrentHashMap<>();
  
  //獲取線程變量  
  T get() {
    return locals.get(Thread.currentThread());
  }
  
  //設置線程變量
  void set(T t) {
    locals.put(Thread.currentThread(), t);
  }
}

那Java的ThreadLocal是這麼實現的嗎?這一次我們的設計思路和Java的實現差異很大。Java的實現裏面也有一個Map,叫做ThreadLocalMap,不過持有ThreadLocalMap的不是ThreadLocal,而是Thread。Thread這個類內部有一個私有屬性threadLocals,其類型就是ThreadLocalMap,ThreadLocalMap 的Key是 ThreadLocal。你可以結合下面的示意圖和精簡之後的Java實現代碼來理解。
在這裏插入圖片描述
在這裏插入圖片描述

class Thread {
  //內部持有ThreadLocalMap
  ThreadLocal.ThreadLocalMap  threadLocals;
}

class ThreadLocal<T>{

  public T get() {
    //首先獲取線程持有的
    //ThreadLocalMap
    ThreadLocalMap map =Thread.currentThread().threadLocals;
    //在ThreadLocalMap中
    //查找變量
    Entry e = map.getEntry(this);
    return e.value;  
  }
  
  static class ThreadLocalMap{
  
    //內部是數組而不是Map
    Entry[] table;
    //根據ThreadLocal查找Entry
    Entry getEntry(ThreadLocal key){
      //省略查找邏輯
    }
    //Entry定義
    static class Entry extends WeakReference<ThreadLocal>{
      Object value;
    }
  }
}

在這裏插入圖片描述
在這裏插入圖片描述
初看上去,我們的設計方案和Java的實現僅僅是Map的持有方不同而已,我們的設計裏面Map屬於ThreadLocal,而Java的實現裏面ThreadLocalMap則是屬於Thread。這兩種方式哪種更合理呢?很顯然Java的實現更合理一些。在Java的實現方案裏面,ThreadLocal僅僅是一個代理工具類,內部並不持有任何與線程相關的數據,所有和線程相關的數據都存儲在Thread裏面,這樣的設計容易理解。而從數據的親緣性上來講,ThreadLocalMap屬於Thread也更加合理。

當然還有一個更加深層次的原因,那就是不容易產生內存泄露。在我們的設計方案中,ThreadLocal 持有的Map會持有Thread對象的引用,這就意味着,只要ThreadLocal對象存在,那麼Map中的Thread對象就永遠不會被回收。ThreadLocal的生命週期往往都比線程要長,所以這種設計方案很容易導致內存泄露。而Java的實現中Thread持有ThreadLocalMap,而且ThreadLocalMap裏對ThreadLocal的引用還是弱引用(WeakReference),所以只要Thread對象可以被回收,那麼ThreadLocalMap就能被回收。Java的這種實現方案雖然看上去複雜一些,但是更加安全。

Java的ThreadLocal實現應該稱得上深思熟慮了,不過即便如此深思熟慮,還是不能百分百地讓程序員避免內存泄露,例如在線程池中使用ThreadLocal,如果不謹慎就可能導致內存泄露。

ThreadLocal與內存泄露

在線程池中使用ThreadLocal爲什麼可能導致內存泄露呢?原因就出在線程池中線程的存活時間太長,往往都是和程序同生共死的,這就意味着Thread持有的ThreadLocalMap一直都不會被回收,再加上ThreadLocalMap中的Entry對ThreadLocal是弱引用(WeakReference),所以只要ThreadLocal結束了自己的生命週期是可以被回收掉的。**但是Entry中的Value卻是被Entry強引用的,**所以即便Value的生命週期結束了,Value也是無法被回收的,從而導致內存泄露。

那在線程池中,我們該如何正確使用ThreadLocal呢?其實很簡單,既然JVM不能做到自動釋放對Value的強引用,那我們手動釋放就可以了。如何能做到手動釋放呢?估計你馬上想到try{}finally{}方案了,這個簡直就是手動釋放資源的利器。示例的代碼如下,你可以參考學習。

ExecutorService es;
ThreadLocal tl;

es.execute(()->{
  //ThreadLocal增加變量
  tl.set(obj);
  
  try {
    // 省略業務邏輯代碼
  }finally {
    //手動清理ThreadLocal 
    tl.remove();
  }
});

InheritableThreadLocal與繼承性

通過ThreadLocal創建的線程變量,其子線程是無法繼承的。也就是說你在線程中通過ThreadLocal創建了線程變量V,而後該線程創建了子線程,你在子線程中是無法通過ThreadLocal來訪問父線程的線程變量V的。

如果你需要子線程繼承父線程的線程變量,那該怎麼辦呢?其實很簡單,Java提供了InheritableThreadLocal來支持這種特性,InheritableThreadLocal是ThreadLocal子類,所以用法和ThreadLocal相同,這裏就不多介紹了。

不過,我完全不建議你在線程池中使用InheritableThreadLocal,不僅僅是因爲它具有ThreadLocal相同的缺點——可能導致內存泄露,更重要的原因是:線程池中線程的創建是動態的,很容易導致繼承關係錯亂,如果你的業務邏輯依賴InheritableThreadLocal,那麼很可能導致業務邏輯計算錯誤,而這個錯誤往往比內存泄露更要命。

總結

線程本地存儲模式本質上是一種避免共享的方案,由於沒有共享,所以自然也就沒有併發問題。如果你需要在併發場景中使用一個線程不安全的工具類,最簡單的方案就是避免共享。避免共享有兩種方案一種方案是將這個工具類作爲局部變量使用,另外一種方案就是線程本地存儲模式。這兩種方案,局部變量方案的缺點是在高併發場景下會頻繁創建對象,而線程本地存儲方案,每個線程只需要創建一個工具類的實例,所以不存在頻繁創建對象的問題。

線程本地存儲模式是解決併發問題的常用方案,所以Java SDK也提供了相應的實現:ThreadLocal。通過上面我們的分析,你應該能體會到Java SDK的實現已經是深思熟慮了,不過即便如此,仍不能盡善盡美,例如在線程池中使用ThreadLocal仍可能導致內存泄漏,所以使用ThreadLocal還是需要你打起精神,足夠謹慎。

實際工作中,有很多平臺型的技術方案都是採用ThreadLocal來傳遞一些上下文信息,例如Spring使用ThreadLocal來傳遞事務信息。

Demo

自己實現
/**
 * 始終已當前線程作爲Key值
 *
 * @param <T>
 */
public class ThreadLocalSimulator<T> {//模擬器

    private final Map<Thread, T> storage = new HashMap<>();//非線程安全的集合類

    public void set(T t) {
        synchronized (this) {
            Thread key = Thread.currentThread();
            storage.put(key, t);
        }
    }


    public T get() {
        synchronized (this) {
            Thread key = Thread.currentThread();
            T value = storage.get(key);
            if (value == null) {
                return initialValue();
            }
            return value;
        }
    }

    public T initialValue() {
        return null;
    }
}
public class ContextTest {
    public static void main(String[] args) {
        IntStream.range(1, 5)
                .forEach(i ->
                        new Thread(new ExecutionTask()).start()
                );
    }
}

public class ExecutionTask implements Runnable {

    private QueryFromDBAction queryAction = new QueryFromDBAction();
    private QueryFromHttpAction httpAction = new QueryFromHttpAction();

    @Override
    public void run() {

        queryAction.execute();//這一步的查詢結果set到ThreadLocal中
        System.out.println("The name query successful");
        httpAction.execute();//取出set的結果做自己的業務
        System.out.println("The card id query successful");

        Context context = ActionContext.getActionContext().getContext();
        System.out.println("The Name is " + context.getName() +
         " and CardId " + context.getCardId());
    }
}

public class QueryFromDBAction {

    public void execute() {
        try {
            Thread.sleep(1000L);
            String name = "Alex " + Thread.currentThread().getName();
            ActionContext.getActionContext().getContext().setName(name);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class QueryFromHttpAction {

    public void execute() {
        Context context = ActionContext.getActionContext().getContext();
        String name = context.getName();
        String cardId = getCardId(name);
        context.setCardId(cardId);
    }
    private String getCardId(String name) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return "435467523543" + Thread.currentThread().getId();
    }
}

public final class ActionContext { //寫成final好些,工具類來說的話

    private static final ThreadLocal<Context> threadLocal = 
                  new ThreadLocal<Context>() {
        @Override
        protected Context initialValue() {
            return new Context();
        }
    };

    private static class ContextHolder {
        private final static ActionContext actionContext = new ActionContext();
}

    public static ActionContext getActionContext() {
        return ContextHolder.actionContext;
    }

    public Context getContext() {
        return threadLocal.get();
    }

    private ActionContext(){

    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章