1. 程式人生 > 其它 >JDK成長記12:ThreadLocal (下)

JDK成長記12:ThreadLocal (下)

上一節你弄懂了ThreadLocal是什麼、它的基本使用方式、get方法的底層原理。這一節讓繼續深入研究下:

  • ThreadLocal的set原始碼原理
  • JVM的中的強引用、弱引用、軟引用、虛引用
  • 弱引用在ThreadLocal的應用
  • ThreadLocal記憶體洩漏問題分析
  • ThreadLocal應用場景舉例

ThreadLocal set方法原始碼原理

ThreadLocal set方法原始碼原理

你有了閱讀threadLocal的get方法的經驗,set方法的原始碼會變得非常簡單。set原始碼如下所示:

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

上面的脈絡是不是很清楚,相信我都不需要畫圖大家就能理解,和上一節get方法呼叫的setInitialValue幾乎一模一樣,只是沒有了initialValue()方法而已。

如果當前執行緒第一次使用threadLcoal.set(Obejct),(假設當前執行緒之前也沒有呼叫過get方法),就會建立一個預設大小為16的threadLocalMap,並且將key設為threadLocal物件,value設定為對應的某個Object。

如果是第二次set肯走的是map.set(this, value);這句話的分支,直接向當前執行緒的threadLocalMap中設定一個key-value對。

如下圖所示:

JVM中的強引用、弱引用、軟引用、虛引用基礎知識

JVM中的強引用、弱引用、軟引用、虛引用基礎知識

你還記得ThreadLocalMap這個每個Thread都有的本地變數嗎?這個Map中的核心的資料結構是一個Entry,代表了Key-Value對的資料,Key值是ThreadLocal物件,value是儲存的物件資料。程式碼如下所示:

 static class ThreadLocalMap {
   static class Entry extends WeakReference<ThreadLocal<?>> {
     /** The value associated with this ThreadLocal. */
     Object value;
     Entry(ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
     }
   }
 }

這個Entry繼承了一個WeakReference的物件。如果熟悉JVM的同學,可能瞭解這個物件是什麼,它被稱作弱引用。

在java中,物件引用可以強引用、軟引用、弱引用、虛引用四種,是jvm回收記憶體判斷的重要標準之一。下面我簡單給大家介紹下他們是什麼,一般應用在什麼場景。

強引用StrongReference,一般宣告的一個物件,都是強引用。使用場景,比如 Loan l = new Loan(); l就是一個強引用。gc如果發現一個物件被強引用指向,如果JVM空間不足的時候,就算OOM也不會回收它。

軟引用SoftReference,當JVM空間不夠的時候,gc會先回收軟引用的空間。使用場景:適合用於快取。

舉個例子:Andriod用Map快取點陣圖資料。

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();


public void addBitmapToCache(String path) {
    // 強引用的Bitmap物件
    Bitmap bitmap = BitmapFactory.decodeFile(path);

    // 軟引用的Bitmap物件
    SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);

    // 新增該物件到Map中使其快取
    imageCache.put(path, softBitmap);
  }

弱引用WeakReference,只要gc發現了弱引用,就會回收掉它的空間。使用場景:ThreadLocalMap, WeakHashMap中的Entry。。

舉個例子:ThreadLocalMap中的entry,這個一會我們重點分析這裡的原理,為什麼這麼做。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
 
    Entry(ThreadLocal<?> k, Object v) {
      super(k);
      value = v;
    }
  } 

虛引用PhantomReference,這個引用在gc垃圾回收執行緒看來,就是沒有引用的意思,它的作用是幫助JVM管理直接記憶體DirectBuffer。經典的使用場景:NIO。

舉個例子:比如DirectBuffer中的Cleaner就是繼承了PhantomReference。

public abstract interface DirectBuffer {

 public abstract long address(); 

 public abstract java.lang.Object attachment();

 public abstract sun.misc.Cleaner cleaner();

} 

public class Cleaner extends java.lang.ref.PhantomReference { 

 private static final java.lang.ref.ReferenceQueue<java.lang.Object> dummyQueue;

   //省略

}

上面這四種引用我給大家畫一個圖,更好理解,如下圖所示:

弱引用在ThreadLocal真能防止記憶體洩漏嗎?

弱引用在ThreadLocal真能防止記憶體洩漏嗎?

瞭解了Java中的四種引用的概念後,我們來看下ThreadLcoalMap中的Entry繼承了WeakReference。到底是為什麼?我們來看如下一個場景。

一個執行緒thread使用threadLocal物件tl設定了一個value為30M的物件,之後tl=null,不再使用了。tl指向的區域threadLocal物件被gc回收。此時會如下圖所示:

這也就解釋了,為什麼ThreadLocalMap的Entry中的key使用弱引用:

因為若是強引用,即使tl=null,key是強引用的話,仍會指向threadLocal,導致threadLocal不會被回收,造成記憶體洩漏。而使用了key弱引用的話,就不會有問題,當tl=null的時候,key是弱引用,gc會直接回收調threadLocal記憶體中的這個物件。雖然使用了弱引用,但是仍存在記憶體value指向的強引用,指向了一個堆中的物件,此時key對應的threadLocal已經回收,key=null,此時,也無法訪問到value了。

所以如果一個set的value如果不在使用或threadLoacal不在使用了,一定要通過remove方法來刪除掉之前的key。不然這麼使用不當,還是會造成記憶體洩漏,導致30M的這個vlaue不會被回收掉

ThreadLocal應用場景

ThreadLocal應用場景

最後給大家提幾個ThreadLocal的應用場景。你可以想一下,ThreadLocal具備這樣特性,可以用在哪裡?

Spring 的Transaction機制中的ThreadLocal

最經典的場景就是Spring 的Transaction機制,將一個執行緒中的事務放入ThreadLocal中,可以在整個方法呼叫棧中隨時取出事務的資訊進行修改和操作,不會影響其他的執行緒的事務。

 // TransactionAspectSupport.java
 private static final ThreadLocal<TransactionInfo> transactionInfoHolder =
 new NamedThreadLocal<TransactionInfo>("Current aspect-driven transaction");

Log4j2等日誌框架中的MDC

  public class LogbackMDCAdapter implements MDCAdapter {
    final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
  }

SpringCloud Sleuth的請求鏈路跟蹤

通過ThreadLocal傳遞Trace資料,值得一提的是,還通過之前提到過的Thread的另一個本地變數副本inheritableThreadLocal。在建立子執行緒的時候,會將父執行緒的inheritableThreadLocals繼承下來,這樣就實現了TraceContext在父子執行緒中的傳遞。程式碼如下:

public static final class Default extends CurrentTraceContext{
  ThreadLocal<TraceContext> DEFAULT = new ThreadLocal<>();

  // Inheritable as Brave 3's ThreadLocalServerClientAndLocalSpanState was inheritable
  static final InheritableThreadLocal<TraceContext> INHERITABLE = new InheritableThreadLocal<>();
 

  final ThreadLocal<TraceContext> local;

}

 

HDFS edits_log的txId自增後放入執行緒本地副本

HDFS每次建立一個檔案,目錄等操作會記錄一條日誌到edits_log中,每條edit_log都有一個txId,會把這個txId記錄到當前執行緒的txId方便在整個執行緒過程中隨時取用,和修改。

/**
 * FSEditLog 維護元資料(檔案目錄樹)也叫名稱空間的修改
*/
@InterfaceAudience.Private
@InterfaceStability.Evolving
public class FSEditLog implements LogsPurgeable {

// stores the most current transactionId of this thread.
private static final ThreadLocal<TransactionId> myTransactionId = new ThreadLocal<TransactionId>() {

 @Override
 protected synchronized TransactionId initialValue() {
  return new TransactionId(Long.MAX_VALUE);
 }

};

private long beginTransaaction() {
 assert Thread.holdsLock(this);
 txid++;
 TransactionId id = myTransactionId.get();
 id.txid = txid;
 return now();
}

還有很多的場景可以使用。其實通過上面的幾個場景,你應該能發現,其實ThreadLocal最常用的2個場景就是:

1、 執行緒中,各個方法需要共享變數時使用。除了方法之間傳遞入參,通過ThreadLocal可以很方便的做到這一點。

2、 多執行緒操作時,防止併發衝突,保證執行緒安全。比如一般會拷貝一份資料到執行緒本地,自己修改本地變數,是執行緒安全的。

好了,今天的成長記就到這裡,你可以在自己的公司遇到的專案中或者開原始碼中找一下或者留意一下。看看它們是怎麼使用ThreadLocal的。

歡迎你在評論區,寫下你遇見的場景。

本文由部落格群發一文多發等運營工具平臺 OpenWrite 釋出