基礎篇:JAVA引用型別和ThreadLocal

潛行前行發表於2021-03-16

前言

平時併發程式設計,除了維護修改共享變數的場景,有時我們也需要為每一個執行緒設定一個私有的變數,進行執行緒隔離,java提供的ThreadLocal可以幫助我們實現,而講到ThreadLocal則不得不講講java的四種引用,不同的引用型別在GC時表現是不一樣的,引用型別Reference有助於我們瞭解如何快速回收某些物件的記憶體或對例項的GC控制

  • 四種引用型別在JVM的生命週期
  • 引用佇列(ReferenceQueue)
  • ThreadLocal的實現原理和使用
  • FinalReference和finalize方法的實現原理
  • Cheaner機制

關注公眾號,一起交流,微信搜一搜: 潛行前行

1 四種引用型別在JVM的生命週期

強引用(StrongReference)

  • 建立一個物件並賦給一個引用變數,強引用有引用變數指向時,永遠也不會垃圾回收,JVM寧願丟擲OutOfMemory異常也不會回收該物件;強引用物件的建立,如
Integer index = new Integer(1);
String name = "csc";
  • 如果中斷所有引用變數和強引用物件的聯絡(將引用變數賦值為null),JVM則會在合適的時間就會回收該物件

軟引用(SoftReference)

  • 和強用引用不同點在於記憶體不足時,該型別引用物件會被垃圾處理器回收
  • 使用軟引用能防止記憶體洩露,增強程式的健壯性。SoftReference的特點是它的一個例項儲存對一個Java物件的軟引用,該軟引用的存在不妨礙垃圾收集執行緒對該Java物件的回收
  • SoftReference類所提供的get()方法返回Java物件的強引用。另外,一旦垃圾執行緒回收該物件之後,get()方法將返回null
    String name = "csc";
    //軟引用的建立
    SoftReference<String> softRef = new SoftReference<String>(name);
    System.out.println(softRef.get());

弱引用(WeakReference)

  • 特點:無論記憶體是否充足,只要進行GC,都會被回收
    String name = "csc";
    //弱引用的建立
    WeakReference<String> softRef = new WeakReference<String>(name);
    System.out.println(softRef.get()); //輸出 csc
    System.gc();
    System.out.println(softRef.get()); //輸出 null
    //弱引用Map
    WeakHashMap<String, String> map = new WeakHashMap<String, String>();

虛引用(PhantomReference)

  • 特點:如同虛設,和沒有引用沒什麼區別;虛引用和軟引用、弱引用不同,它並不決定物件的生命週期。如果一個物件與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收
  • 要注意的是,虛引用必須和引用佇列關聯使用,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收
public static void main(String[] args) {
    String name = "csc";
    ReferenceQueue<String> queue = new ReferenceQueue<String>();  
    PhantomReference<String> pr = new PhantomReference<String>(name, queue);  
    //PhantomRefrence的get方法總是返回null,因此無法訪問對應的引用物件。
    System.out.println(pr.get());  // null
    System.gc();
    System.out.println(queue.poll()); //獲取被垃圾回收的"xb"的引用ReferenceQueue
}
引用型別 被垃圾回收時間 場景 生存時間
強引用 從來不會 物件的一般狀態 JVM停止執行時終止
軟引用 當記憶體不足時 物件快取 記憶體不足時終止
弱引用 正常垃圾回收時 物件快取 垃圾回收後終止
虛引用 正常垃圾回收時 跟蹤物件的垃圾回收 垃圾回收後終止

2 引用佇列(ReferenceQueue)

  • 引用佇列可以配合軟引用、弱引用及虛引用使用;當引用的物件將要被JVM回收時,會將其加入到引用佇列中
  ReferenceQueue<String> queue = new ReferenceQueue<String>();  
  WeakReference<String> pr = new WeakReference<String>("wxj", queue); 
  System.gc();
  System.out.println(queue.poll().get()); // 獲取即將被回收的字串 wxj

3 ThreadLocal的原理和使用

ThreadLocal 的實現原理

  • 每個執行緒都內建了一個ThreadLocalMap物件
public class Thread implements Runnable {
	/* 當前執行緒對於的ThreadLocalMap例項,ThreadLocal<T>作為Key, 
	 * T對應的物件作為value */
	ThreadLocal.ThreadLocalMap threadLocals = null;
  • ThreadLocalMap作為ThreadLocal的內部類,實現了類似HashMap的功能,它元素Entry繼承於WeakReference,key值是ThreadLocal,value是引用變數。也就是說jvm發生GC時value物件則會被回收
public class ThreadLocal<T> {
	//ThreadLocal物件對應的hash值,使用一個靜態AtomicInteger實現
	private final int threadLocalHashCode = nextHashCode();
    //設定value
	public void set(T value) {
        Thread t = Thread.currentThread();  
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    //獲取value
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
          //獲取當前執行緒的ThreadLocalMap,再使用物件ThreadLocal獲取對應的value
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    ....
    //類似HashMap的類
    static class ThreadLocalMap {
    	//使用開放地址法解決hash衝突
        //如果hash出的index已經有值,通過演算法在後面的若干位置尋找空位
    	private Entry[] table;
        ...
        //Entry 是弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
   }     

ThreadLocal不保證共享變數在多執行緒的安全性

  • 從ThreadLocal的實現原理可知,ThreadLocal只是為每個執行緒儲存一個副本變數,副本變數的修改不影響其他執行緒的變數值,因此ThreadLocal不能實現共享變數的安全性

ThreadLocal 使用場景

  • 執行緒安全,包裹執行緒不安全的工具類,比如java.text.SimpleDateFormat類,當然jdk1.8已經給出了對應的執行緒安全的類java.time.format.DateTimeFormatter
  • 執行緒隔離,比如資料庫連線管理、Session管理、mdc日誌追蹤等。

ThreadLocal記憶體洩露和WeakReference

  • ThreadLocalMap.Entry是弱引用,弱引用物件是不管有沒有被引用都會被垃圾回收
  • 發生記憶體洩漏一般是線上程池的執行緒,生命週期長,threadLocals引用會一直存在,當其存放的ThreadLocal被回收(弱引用生命週期短)後,它對應的Entity成了e.get()==null的例項。執行緒不死則Entity一直不會被回收,這就發生了記憶體洩漏
  • 如果執行緒跨業務操作相同的ThreadLocal,還會造成變數安全問題
  • 通常在使用完ThreadLocal最好呼叫它的remove();在ThreadLocal的get、set的時候,最好檢查當前Entity的key是否為null,如果是null就把Entity釋放掉,value則會被垃圾回收

4 finalize方法的實現原理FinalReference

final class Finalizer extends FinalReference<Object> { 
  private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
  	/* Invoked by VM */
   static void register(Object finalizee) {
        new Finalizer(finalizee);
   }
   private static class FinalizerThread extends Thread {
        ....
        public void run() {
            ...
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    //這裡會實現Object.finalize的呼叫
                    f.runFinalizer(jla);
           ....
    }
    
   static {
        ...
        Thread finalizer = new FinalizerThread(tg);
        ... //執行Object.finalize的守護執行緒
        finalizer.setDaemon(true);
        finalizer.start();
    }
    
  • cpu資源比較稀缺的情況下FinalizerThread執行緒有可能因為優先順序比較低而延遲執行finalizer物件的finalize方法
  • 因為finalizer物件的finalize方法遲遲沒有執行,有可能會導致大部分finalizer物件進入到old分代,此時容易引發old分代的gc,甚至fullgc,gc暫停時間明顯變長

5 Cheaner機制

歡迎指正文中錯誤

參考文章

相關文章