你不可不知的Java引用型別之——ReferenceQueue原始碼詳解
定義
ReferenceQueue是引用佇列,用於存放待回收的引用物件。
說明
對於軟引用、弱引用和虛引用,如果我們希望當一個物件被垃圾回收器回收時能得到通知,進行額外的處理,這時候就需要使用到引用隊列了。
在一個物件被垃圾回收器掃描到將要進行回收時,其相應的引用包裝類,即reference物件會被放入其註冊的引用佇列queue中。可以從queue中獲取到相應的物件資訊,同時進行額外的處理。比如反向操作,資料清理,資源釋放等。
使用例子
public class ReferenceQueueTest { private static ReferenceQueue<byte[]> rq = new ReferenceQueue<>(); private static int _1M = 1024 * 1024; public static void main(String[] args) { Object value = new Object(); Map<WeakReference<byte[]>, Object> map = new HashMap<>(); Thread thread = new Thread(ReferenceQueueTest::run); thread.setDaemon(true); thread.start(); for(int i = 0;i < 100;i++) { byte[] bytes = new byte[_1M]; WeakReference<byte[]> weakReference = new WeakReference<>(bytes, rq); map.put(weakReference, value); } System.out.println("map.size->" + map.size()); int aliveNum = 0; for (Map.Entry<WeakReference<byte[]>, Object> entry : map.entrySet()){ if (entry != null){ if (entry.getKey().get() != null){ aliveNum++; } } } System.out.println("100個物件中存活的物件數量:" + aliveNum); } private static void run() { try { int n = 0; WeakReference k; while ((k = (WeakReference) rq.remove()) != null) { System.out.println((++n) + "回收了:" + k); } } catch (InterruptedException e) { e.printStackTrace(); } } }
這裡有一個小栗子,main方法中,建立了一條執行緒,使用死迴圈來從引用佇列中獲取元素,監控物件被回收的狀態。然後迴圈往map中添加了100個對映關係,以下是執行結果:
...前面省略了大量相似輸出 85回收了:java.lang.ref.WeakReference@7106e68e 86回收了:java.lang.ref.WeakReference@1f17ae12 87回收了:java.lang.ref.WeakReference@c4437c4 map.size->100 100個物件中存活的物件數量:12
通過配合使用ReferenceQueue,可以較好的監控物件的生存狀態。
成員變數
ReferenceQueue中內部成員變數也很少,主要有這麼幾個:
static ReferenceQueue<Object> NULL = new Null<>(); static ReferenceQueue<Object> ENQUEUED = new Null<>();
有兩個用來做為特殊標記的靜態成員變數,一個是NULL,一個是ENQUEUE,上一篇中說的ReferenceQueue.NULL和ReferenceQueue.ENQUEUED就是這兩個傢伙。
來看看Null長什麼樣:
private static class Null<S> extends ReferenceQueue<S> { boolean enqueue(Reference<? extends S> r) { return false; } }
只是簡單繼承了ReferenceQueue的一個類,emmm,為什麼不直接new一個ReferenceQueue呢?這裡自然是有它的道理的,如果直接使用ReferenceQueue,就會導致有可能誤操作這個NULL和ENQUEUED變數,因為ReferenceQueue中enqueue方法是需要使用lock物件鎖的,這裡覆蓋了這個方法並直接返回false,這樣就避免了亂用的可能性,也避免了不必要的資源浪費。
static private class Lock { }; private Lock lock = new Lock();
跟Reference一樣,有一個lock物件用來做同步物件。
private volatile Reference<? extends T> head = null;
head用來儲存佇列的頭結點,因為Reference是一個單鏈表結構,所以只需要儲存頭結點即可。
private long queueLength = 0;
queueLength用來儲存佇列長度,在新增元素的時候+1,移除元素的時候-1,因為在新增和移除操作的時候都會使用synchronized進行同步,所以不用擔心多執行緒修改會不會出錯的問題。
內部方法
// 這個方法僅會被Reference類呼叫 boolean enqueue(Reference<? extends T> r) { synchronized (lock) { // 檢測從獲取這個鎖之後,該Reference沒有入隊,並且沒有被移除 ReferenceQueue<?> queue = r.queue; if ((queue == NULL) || (queue == ENQUEUED)) { return false; } assert queue == this; // 將reference的queue標記為ENQUEUED r.queue = ENQUEUED; // 將r設定為連結串列的頭結點 r.next = (head == null) ? r : head; head = r; queueLength++; // 如果r的FinalReference型別,則將FinalRef+1 if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(1); } lock.notifyAll(); return true; } }
這裡是入隊的方法,使用了lock物件鎖進行同步,將傳入的r新增到佇列中,並重置頭結點為傳入的節點。
public Reference<? extends T> poll() { if (head == null) return null; synchronized (lock) { return reallyPoll(); } } private Reference<? extends T> reallyPoll() { Reference<? extends T> r = head; if (r != null) { head = (r.next == r) ? null : r.next; r.queue = NULL; r.next = r; queueLength--; if (r instanceof FinalReference) { sun.misc.VM.addFinalRefCount(-1); } return r; } return null; }
poll方法將頭結點彈出。嗯,沒錯,彈出的是頭結點而不是尾節點,名義上,它叫ReferenceQueue,實際上是一個ReferenceStack(滑稽)。驚不驚喜,意不意外。
/** * 移除並返回佇列首節點,此方法將阻塞到獲取到一個Reference物件或者超時才會返回 * timeout時間的單位是毫秒 */ public Reference<? extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException{ if (timeout < 0) { throw new IllegalArgumentException("Negative timeout value"); } synchronized (lock) { Reference<? extends T> r = reallyPoll(); if (r != null) return r; long start = (timeout == 0) ? 0 : System.nanoTime(); // 死迴圈,直到取到資料或者超時 for (;;) { lock.wait(timeout); r = reallyPoll(); if (r != null) return r; if (timeout != 0) { // System.nanoTime方法返回的是納秒,1毫秒=1納秒*1000*1000 long end = System.nanoTime(); timeout -= (end - start) / 1000_000; if (timeout <= 0) return null; start = end; } } } } /** * 移除並返回佇列首節點,此方法將阻塞到獲取到一個Reference物件才會返回 */ public Reference<? extends T> remove() throws InterruptedException { return remove(0); }
這裡兩個方法都是從佇列中移除首節點,與poll不同的是,它會阻塞到超時或者取到一個Reference物件才會返回。
聰明的你可能會想到,呼叫remove方法的時候,如果佇列為空,則會一直阻塞,也會一直佔用lock物件鎖,這個時候,有引用需要入隊的話,不就進不來了嗎?
嗯,講道理確實是這樣的,但是注意註釋,enqueue只是給Reference呼叫的,在Reference的public方法enqueue中可以將該引用直接入隊,但是虛擬機器作為程式的管理者可不吃這套,而是通過其它方式將Reference物件塞進去的,所以才會出現之前的栗子中,死迴圈呼叫remove方法,並不會阻塞引用進入佇列中的情況。
應用場景
ReferenceQueue一般用來與SoftReference、WeakReference或者PhantomReference配合使用,將需要關注的引用物件註冊到引用佇列後,便可以通過監控該佇列來判斷關注的物件是否被回收,從而執行相應的方法。
主要使用場景:
1、使用引用佇列進行資料監控,類似前面栗子的用法。
2、佇列監控的反向操作
反向操作,即意味著一個數據變化了,可以通過Reference物件反向拿到相關的資料,從而進行後續的處理。下面有個小栗子:
public class TestB { private static ReferenceQueue<byte[]> referenceQueue = new ReferenceQueue<>(); private static int _1M = 1024 * 1024; public static void main(String[] args) throws InterruptedException { final Map<Object, MyWeakReference> hashMap = new HashMap<>(); Thread thread = new Thread(() -> { try { int n = 0; MyWeakReference k; while(null != (k = (MyWeakReference) referenceQueue.remove())) { System.out.println((++n) + "回收了:" + k); //反向獲取,移除對應的entry hashMap.remove(k.key); //額外對key物件作其它處理,比如關閉流,通知操作等 } } catch(InterruptedException e) { e.printStackTrace(); } }); thread.setDaemon(true); thread.start(); for(int i = 0;i < 10000;i++) { byte[] bytesKey = new byte[_1M]; byte[] bytesValue = new byte[_1M]; hashMap.put(bytesKey, new MyWeakReference(bytesKey, bytesValue, referenceQueue)); } } static class MyWeakReference extends WeakReference<byte[]> { private Object key; MyWeakReference(Object key, byte[] referent, ReferenceQueue<? super byte[]> q) { super(referent, q); this.key = key; } } }
這裡通過referenceQueue監控到有引用被回收後,通過map反向獲取到對應的value,然後進行資源釋放等。
小結
- ReferenceQueue是用來儲存需要關注的Reference佇列
- ReferenceQueue內部實現實際上是一個棧
- ReferenceQueue可以用來進行資料監控,資源釋放等