定义
引用队列是用于存储要回收的引用对象的引用队列。
说明
对于软引用、弱引用和虚拟引用,如果希望在垃圾收集器回收对象以进行其他处理时得到通知,则需要使用引用队列。
当垃圾收集器扫描要回收的对象时,将对应的引用包装器类(引用对象)放入其注册的引用队列队列中。可以从队列中获得相应的对象信息,并且可以同时进行附加处理。例如,反向操作、数据清理、资源释放等。
使用例子
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();
}
}
}
这里有一个小栗子。在主方法中,创建一个线程,该线程使用死循环从引用队列中提取元素并监视正在回收的对象的状态。然后循环向映射添加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。ReeferenceQueue。NULL和ReferenceQueue。前面的文章中提到的是这两个人。
来看看Null长什么样:
private static class Null<S> extends ReferenceQueue<S> {
boolean enqueue(Reference<? extends S> r) {
return false;
}
}
简单地继承了一类ReferenceQueue,emmm,为什么不直接新建一个ReferenceQueue呢?这是有原因的。如果直接使用Reference Queue,则可能会误操作NULL和ENQUEUED变量,因为Reference Queue中的队列方法需要使用锁对象锁,锁对象锁覆盖此方法并直接返回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,因为同步同步用于同步添加和删除操作,所以不必担心多线程修改的错误。
内部方法
// 这个方法仅会被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;
}
}
下面是加入队列的方法,使用锁对象锁进行同步,将传入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;
}
轮询方法弹出头节点。是的,弹出的是头节点,而不是尾节点。名义上,它被称为引用队列。实际上是参考资料库。不奇怪,不奇怪。
/**
* 移除并返回队列首节点,此方法将阻塞到获取到一个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对象才会返回。
您可能认为在调用远程方法时,如果队列是空的,那么它总是会阻塞并占用锁对象锁。此时,如果有引用需要加入队列,您不能进来吗?
这个推理是正确的,但是请注意,队列仅用于引用。在Reference的公共方法队列中,可以直接对引用进行排队,但是作为程序的虚拟机的管理器不能吃掉这个集合,而是用其他方式填充Reference对象,因此它会出现在板栗之前,死循环调用远程方法。不阻塞进入队列的引用。
应用场景
引用队列通常与软引用、弱引用或幻影引用一起使用。在将需要注意的引用对象注册到引用队列中之后,我们可以监视该队列以确定感兴趣的对象是否被回收,然后执行相应的方法。
主要使用场景:
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可以用来进行数据监控,资源释放等