天天看點

基礎篇:JAVA引用類型和ThreadLocal

前言

平時并發程式設計,除了維護修改共享變量的場景,有時我們也需要為每一個線程設定一個私有的變量,進行線程隔離,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機制

  • 上一篇文章有介紹到jdk1.8的Cleaner架構篇:ByteBuffer和netty.ByteBuf詳解

歡迎指正文中錯誤

參考文章

  • ThreadLocal原理及使用場景大揭秘
  • JDK源碼分析之FinalReference完全解讀
  • 一次 Young GC 的優化實踐
  • Netty資源洩露檢測
  • 避免使用Finalizer和Cleaner機制
  • 【JAVA Reference】Cleaner 源碼剖析(三)