你不可不知的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可以用來進行數據監控,資源釋放等


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM