定义

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可以用来进行数据监控,资源释放等

你不可不知的Java引用类型之——ReferenceQueue源码详解的更多相关文章

  1. 你不可不知的Java引用类型之——SoftReference源码详解

    定义 SoftReference是软引用,其引用的对象在内存不足的时候会被回收.只有软引用指向的对象称为软可达(softly-reachable)对象. 说明 垃圾回收器会在内存不足,经过一次垃圾回收 ...

  2. 你不可不知的Java引用类型之——PhantomReference源码详解

    定义 PhantomReference是虚引用,该引用不会影响不会影响对象的生命周期,也无法从虚引用中获取对象实例. 说明 源码介绍部分其实也没多大内容,主要内容都在前面介绍中说完了.PhantomR ...

  3. 你不可不知的Java引用类型之——WeakReference源码详解

    定义 WeakReference是弱引用,该引用不会影响垃圾回收器对对象的回收,不会影响对象的生命周期. 说明 当虚拟机在某个时间点决定要回收一个弱可达(weakly-reachable)对象时,会自 ...

  4. 数据结构与算法系列2 线性表 链表的分类+使用java实现链表+链表源码详解

    数据结构与算法系列2.2 线性表 什么是链表? 链表是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表的链接次序实现的一系列节点组成,节点可以在运行时动态生成,每个节点包括两个 ...

  5. 你不可不知的Java引用类型之——Reference源码解析

    定义 Reference是所有引用类型的父类,定义了引用的公共行为和操作. reference指代引用对象本身,referent指代reference引用的对象,下文介绍会以reference,ref ...

  6. 数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解

    数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解 对数组有不了解的可以先看看我的另一篇文章,那篇文章对数组有很多详细的解析,而本篇文章则着重讲动态数组,另一篇文章链接 ...

  7. Java集合【6.1】-- Collection接口源码详解

    目录 一.Collection接口简介 二.Collection源码分析 三.Collection的子类以及子类的实现 3.1 List extend Collection 3.2 Set exten ...

  8. Java多线程学习之线程池源码详解

    0.使用线程池的必要性 在生产环境中,如果为每个任务分配一个线程,会造成许多问题: 线程生命周期的开销非常高.线程的创建和销毁都要付出代价.比如,线程的创建需要时间,延迟处理请求.如果请求的到达率非常 ...

  9. Java集合——TreeMap源码详解

    )TreeMap 是一个有序的key-value集合,它是通过红黑树实现的.因为红黑树是平衡的二叉搜索树,所以其put(包含update操作).get.remove的时间复杂度都为log(n). (2 ...

随机推荐

  1. spring boot整合 springmvc+mybatis

    需要以下依赖 <dependencies> <dependency> <groupId>org.springframework.boot</groupId&g ...

  2. 【shiro】(4)---Shiro认证、授权案例讲解

    Shiro认证.授权案例讲解 一.认证  1. 认证流程     2.用户密码已经加密.加盐的用户认证 (1)测试类 // 用户登陆和退出,这里我自定了一个realm(开发肯定需要自定义realm获取 ...

  3. javaScript笔记详解(1)

    javaScript基础详解 版权声明 本文原创作者:雨点的名字 作者博客地址:https://home.cnblogs.com/u/qdhxhz/ 首先讲javaScript的摆放位置:<sc ...

  4. (转载)java内存模型

    java并发采用的是共享内存模型,线程之间的通信对程序员来说是透明的,内存可见性问题很容易困扰着java程序员,今天我们就来揭开java内存模型的神秘面纱. 在揭开面纱之前,我们需要认识几个基础概念: ...

  5. 【EF6学习笔记】(三)排序、过滤查询及分页

    本篇原文地址:Sorting, Filtering, and Paging 说明:学习笔记参考原文中的流程,为了增加实际操作性,并能够深入理解,部分地方根据实际情况做了一些调整:并且根据自己的理解做了 ...

  6. 获取完全一样的数据库,包括表与表之间的外键关系,check,default表结构脚本

    今天公司给了一个任务,某一个项目由于数据过大,造成Sql Server 2012 的运行占用很大内存,于是要把之前的不常用的数据分开.要求写个脚本,要求: 1.能获取原来数据库中的表结构,主键一致.表 ...

  7. Ubuntu16.04环境下搭建基于三台主机的mysql galera cluster集群(实测有效)

    (注意: (1)文中红色字体部分不一定需要操作 (2)由于word文档编辑的原因,实际操作时部分命令需要手动输入!!直接复制粘贴会提示错误!! ) 一  搭建环境: 1 Ubuntu16.04版本(系 ...

  8. kubelet集群网络配置flannel(覆盖网络)

    kubernetes本身并不会对跨主机容器的网络进行设置,这需要额外的工具来实现.一些常用的开源工具主要包括flanne.OpenvSwitch.Weave.Calico等,这里面最常用的是flann ...

  9. springmvc 项目完整示例09 maven项目创建

    需求表均同springmvc案例 此处只是使用maven 注意,以下所有需要建立在你的eclipse等已经集成配置好了maven了,说白了就是新建项目的时候已经可以找到maven了 没有的话需要安装m ...

  10. zookeeper配置中心实战--solrcloud zookeeper配置中心原理及源码分析

    程序的发展,需要引入集中配置: 随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关.参数的配置.服务器的地址…… 并且对配置的期望也越来越高,配置修改后实时生效,灰度发布,分环境.分集群管理配 ...