你不可不知的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可以用来进行数据监控,资源释放等
你不可不知的Java引用类型之——ReferenceQueue源码详解的更多相关文章
- 你不可不知的Java引用类型之——SoftReference源码详解
定义 SoftReference是软引用,其引用的对象在内存不足的时候会被回收.只有软引用指向的对象称为软可达(softly-reachable)对象. 说明 垃圾回收器会在内存不足,经过一次垃圾回收 ...
- 你不可不知的Java引用类型之——PhantomReference源码详解
定义 PhantomReference是虚引用,该引用不会影响不会影响对象的生命周期,也无法从虚引用中获取对象实例. 说明 源码介绍部分其实也没多大内容,主要内容都在前面介绍中说完了.PhantomR ...
- 你不可不知的Java引用类型之——WeakReference源码详解
定义 WeakReference是弱引用,该引用不会影响垃圾回收器对对象的回收,不会影响对象的生命周期. 说明 当虚拟机在某个时间点决定要回收一个弱可达(weakly-reachable)对象时,会自 ...
- 数据结构与算法系列2 线性表 链表的分类+使用java实现链表+链表源码详解
数据结构与算法系列2.2 线性表 什么是链表? 链表是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表的链接次序实现的一系列节点组成,节点可以在运行时动态生成,每个节点包括两个 ...
- 你不可不知的Java引用类型之——Reference源码解析
定义 Reference是所有引用类型的父类,定义了引用的公共行为和操作. reference指代引用对象本身,referent指代reference引用的对象,下文介绍会以reference,ref ...
- 数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解
数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解 对数组有不了解的可以先看看我的另一篇文章,那篇文章对数组有很多详细的解析,而本篇文章则着重讲动态数组,另一篇文章链接 ...
- Java集合【6.1】-- Collection接口源码详解
目录 一.Collection接口简介 二.Collection源码分析 三.Collection的子类以及子类的实现 3.1 List extend Collection 3.2 Set exten ...
- Java多线程学习之线程池源码详解
0.使用线程池的必要性 在生产环境中,如果为每个任务分配一个线程,会造成许多问题: 线程生命周期的开销非常高.线程的创建和销毁都要付出代价.比如,线程的创建需要时间,延迟处理请求.如果请求的到达率非常 ...
- Java集合——TreeMap源码详解
)TreeMap 是一个有序的key-value集合,它是通过红黑树实现的.因为红黑树是平衡的二叉搜索树,所以其put(包含update操作).get.remove的时间复杂度都为log(n). (2 ...
随机推荐
- homebrew 更改镜像,进行成功安装
在mac系统中,使用homebrew可以很方便的管理包.按照官网的说明执行以下命令时总是报错: /usr/bin/ruby -e "$(curl -fsSL https://raw.gith ...
- 使用 DryIoc 替换 Abp 的 DI 框架
一.背景 你说我 Castle Windsor 库用得好好的,为啥要大费周章的替换成 DryIoc 库呢?那就是性能,DryIoc 是一款优秀而且轻量级的 DI 框架,整个项目代码就两个文件,加起来代 ...
- JVM读书笔记之内存管理
对于从事C.C++程序开发人员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”又是从事最基础工作的“劳动人民”--既拥有每一个对象的“所有权”,又负责每一个对象生命开始到终结的维护责任. 对于Ja ...
- .NET西安社区 [拥抱开源,又见 .NET] 活动简报
拥抱开源, 又见 .NET」 随着 .NET Core的发布和开源,.NET又重新回到了人们的视野.除了开源.跨平台.高性能以及优秀的语言特性,越来越多的第三方开源库也出现在了Github上——包括M ...
- java web路径和spring读取配置文件
此篇博客缘起:部署java web系统到阿里云服务器(ubuntu14.04)的时候,有以下两个问题 找不到自定义的property配置文件 上传图片的时候找不到路径 开发的时候是在windows上的 ...
- Linux官方源、镜像源汇总
本文收录在日常运维杂烩系列 一.站点版 1.企业站 搜狐:http://mirrors.sohu.com/ 网易:http://mirrors.163.com/ 阿里云:http://mirrors. ...
- 浅谈缓存技术在ASP.NET中的运用
本篇文章虽不谈架构,但是Cache又是架构中不可或缺的部分,因此,在讲解Cache的同时,将会提及到部分架构知识,关于架构部分,读者可以不用理解,或者直接跳过, 你只需关心Cache即可,具体的架构, ...
- python重试库retryiny源码剖析
上篇博文介绍了常见需要进行请求重试的场景,本篇博文试着剖析有名的python第三方库retrying源码. 在剖析其源码之前,有必要讲一下retrying的用法,方便理解. 安装: pip insta ...
- Python多进程操作同一个文件,文件锁问题
最近工作当中做了一个项目,这个项目主要是操作文件的. 在操作耗时操作的时候,我们一般采用多线程或者多进程.在开发中,如果多个线程需要对文件进行读写操作,就需要用到线程锁或者是文件锁. 使用fcntl ...
- 序言:我为什么学Perl
曾经,我熟练操作grep.awk.sed,甚至自认对sed尚算精通,我一度爱上了写脚本.但是随着写脚本的次数多了,需求复杂了,我深深的感受到shell的无奈. 例如,我多次遇到过类似下面这种恶心的需求 ...