支持 equals 相等的对象(可重复对象)作为 WeakHashMap 的 Key
问题
长链接场景下通常有一个类似 Map<String, Set<Long>> 的结构,用来查找一个逻辑组内的哪些用户,String 类型的 Entry.key 是逻辑组 key,Set<Long> 类型的 Entry.value 存放逻辑组内的用户 Id,那么这个 Map 显然要在逻辑组内用户为 0 时删除这个 Entry,以避免内存泄漏。
删除 Map 的 value 很容易联想到 remove,但并发的处理很复杂,还要单独开一个线程,如果可以自动删除就好了,而 WeakHashMap 就可以自动删除 value,前提它是 Entry.key 不存在引用时删除 Entry.value,那么只要将用户的生命周期和 Entry.key 关联上即可,以 Netty 的 Channel 为例就是将该 Entry.key 放到 Channel.attr 中。
上面稍微一看就有问题,Entry.key 是一个 String 类型的变量,字符串存在常量池(字符串其实挺好的),Channel 就算销毁了也不会丢失对 WeakHashMap Entry.value 的引用,如果每次都 new 一个对象呢?问题更大,此时只有第一个用户强引用 WeakHashMap 的 Entry.value(即 new Set 再 add),其他用户仅仅是获取到了(此时 Entry.key 是第一个用户的,而不是当前用户的),这样第一个用户下线时,这个 Set 就会被 GC。显而易见问题是 Entry.Key 引用不一致导致的,只要给用户返回永远相同的 Entry.key 即可。
如何返回永远相同的对象呢?感觉又回到了原点,因为返回一样的对象显然是 Map<String, Object>,但这个 Map 同样不能内存泄漏,不过情况略有不同,区别在于查找 Set 变成了一个嵌套的查找(String -> Object -> Set<Long>),而用户强引用的 Entry.key 变成了 Object,即 Object 对象的生命周期跟随用户走即可( WeakHashMap<Object, Set<Long>> 负责 GC Set),也就是 WeakHashMap<Object, WeakReference<Object>>。
解决
下面给出代码:
package io.github.hligaty.util;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Recreatable key objects.
* With recreatable key objects,
* the automatic removal of WeakHashMap entries whose keys have been discarded may prove to be confusing,
* but WeakKey will not.
*
* @param <K> the type of keys maintained
* @author hligaty
* @see java.util.WeakHashMap
*/
public class WeakKey<K> {
private static final WeakHashMap<WeakKey<?>, WeakReference<WeakKey<?>>> cache = new WeakHashMap<>();
private static final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
private static final WeakHashMap<Thread, WeakKey<?>> shadowCache = new WeakHashMap<>();
private static final ReadWriteLock shadowCacheLock = new ReentrantReadWriteLock();
private K key;
private WeakKey() {
}
@SuppressWarnings("unchecked")
public static <T> WeakKey<T> wrap(T key) {
WeakKey<T> shadow = (WeakKey<T>) getShadow();
shadow.key = key;
cacheLock.readLock().lock();
try {
WeakReference<WeakKey<?>> ref = cache.get(shadow);
if (ref != null) {
shadow.key = null;
return (WeakKey<T>) ref.get();
}
} finally {
cacheLock.readLock().unlock();
}
cacheLock.writeLock().lock();
try {
WeakReference<WeakKey<?>> newRef = cache.get(shadow);
shadow.key = null;
if (newRef == null) {
WeakKey<T> weakKey = new WeakKey<>();
weakKey.key = key;
newRef = new WeakReference<>(weakKey);
cache.put(weakKey, newRef);
return weakKey;
}
return (WeakKey<T>) newRef.get();
} finally {
cacheLock.writeLock().unlock();
}
}
private static WeakKey<?> getShadow() {
Thread thread = Thread.currentThread();
shadowCacheLock.readLock().lock();
WeakKey<?> shadow;
try {
shadow = shadowCache.get(thread);
if (shadow != null) {
return shadow;
}
} finally {
shadowCacheLock.readLock().unlock();
}
shadowCacheLock.writeLock().lock();
try {
shadow = shadowCache.get(thread);
if (shadow == null) {
shadow = new WeakKey<>();
shadowCache.put(thread, shadow);
return shadow;
}
return shadow;
} finally {
shadowCacheLock.writeLock().unlock();
}
}
public K unwrap() {
return key;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WeakKey<?> weakKey = (WeakKey<?>) o;
return Objects.equals(key, weakKey.key);
}
@Override
public int hashCode() {
return Objects.hash(key);
}
@Override
public String toString() {
return "WeakKey{" +
"attr=" + key +
'}';
}
}
WeakKey 是前面说的 Object,使用时将需要释放的数据 Data 放到以 WeakKey 为 key 的 WeakHashMap(WeakHashMap<WeakKey, Data>),这样当全部用户释放 WeakKey 引用时就可以完成 WeakHashMap Entry 的 GC(包括 WeakKey 和 Data)。
WeakKey 的主要工作是将用户传入的 key 封装一下再返回,保证全局唯一和内存安全,核心结构是 WeakHashMap<WeakKey<?>, WeakReference<WeakKey<?>>> cache,Entry.key 是对用户 key 封装的 WeakKey,Entry.value 是 Entry.key 外层嵌套的 WeakReference,作用是避免 value 对 key 强引用而无法对 Entry GC。因此 cache 只要没人强引用里面的 WeakKey,这个 map 在 GC 后就是空的,这样就完成了目标,其余的就是优化了。
如果想在 cache 里查到 WeakKey,那么首先要新建一个 WeakKey,再把 key 赋值到 WeakKey 中,再通过这个新建的 WeakKey 查找,像下面一样:
String key = "key";
WeakKey<String> weakKey = new WeakKey<>();
weakKey.key = key;
WeakReference<WeakKey<?>> ref = cache.get(weakKey);
每次查找都新建对象,有点沙雕,这里使用缓存对象赋值再查找就可以,另外要保证线程安全,threadLocal 没大问题(ThreadLocal.withInitial(WeakKey::new)),只是不能在 finally 里 remove(remove 的话下次还得新建),在线程池里使用问题不大,不过还有另一种办法,就是 WeakHashMap<Thread, WeakKey<?>>,它可以保证这个缓存中的“影子”对象在这个线程只创建一次,当线程被 GC 的同时删除“影子”对象,与 threadLocal 的区别只是牺牲了一些加读锁的时间。
测试
下面的 WeakHashMap put 了 Arrays.asList(705, 630, 818) 和 Collections.singletonList(705630818) 两个数据,只有后面的 key 被方法引用了,因此在 GC 后 前一个 key 在 map 中找不到 value,而后一个 key 能获取到 value。
package io.github.hligaty.util;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.*;
class WeakKeyTest {
@Test
public void testWeakKey() throws InterruptedException {
WeakHashMap<WeakKey<List<Integer>>, Object> map = new WeakHashMap<>();
map.put(WeakKey.wrap(Arrays.asList(705, 630, 818)), new Object());
WeakKey<List<Integer>> weakKey = WeakKey.wrap(Collections.singletonList(705630818));
map.put(weakKey, new Object());
System.gc();
Thread.sleep(5000L);
Assertions.assertNull(map.get(WeakKey.wrap(Arrays.asList(705, 630, 818))));
Assertions.assertNotNull(map.get(WeakKey.wrap(Collections.singletonList(705630818))));
}
}
其他
如果你想使用 null,那 WeakKey 是支持的,但需要注意一点,如果你有两个不同类型的 key 使用了 WeakKey,而两者都允许 WeakKey.wrap(null),那么当有一个类型的使用者持有 WeakKey.wrap(null),另一个类型的 WeakKey.wrap(null) 是不会被释放的,因为显然 null == null
支持 equals 相等的对象(可重复对象)作为 WeakHashMap 的 Key的更多相关文章
- Java中的Set集合接口实现插入对象不重复的原理
在java的集合中,判断两个对象是否相等的规则是: 1).判断两个对象的hashCode是否相等 . 如果不相等,认为两个对象也不相等,完毕 如果相等,转入2)(这一点只是为了提 ...
- java中的ArrayList 使得集合中的对象不重复
JAVA中的List接口存放的元素是可以重复的,在这个我重写对象里面的equals()方法,让集合里存放的对象不能重复 首先建一个类,在里面的main()方法中实现 list1中存放的是可以重复对象的 ...
- list集合去除重复对象的实现
下面小编就为大家带来一篇list集合去除重复对象的实现.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧 对象重复是指对象里面的变量的值都相等,并不定是地址.list集合存 ...
- 判断ArryaList有没有重复对象的方法
ArrayList类是List类下一种常用的子类,如果要判断容器里面的对象是否有相等,有两种方法. 下面是自定义的一个Student类,假设容器里重复是按照对象的两个属性都相等. /** * @aut ...
- JAVA中List对象去除重复值的方法
JAVA中List对象去除重复值,大致分为两种情况,一种是List<String>.List<Integer>这类,直接根据List中的值进行去重,另一种是List<Us ...
- java:Set对象TreeSet有序子类,HashSet无序子类,重复对象二
TreeSet有序子类; HashSet无序子类 重复重复元素,Object对象是通过equals和hashCode来进行过滤的. 如果将上一篇提到中的例子中的TreeSet,换成HashSet,那么 ...
- js数组中去除重复对象及去除空对象的方法
(function(){//去除数组中重复对象 var unique = {}; arr.forEach(function(a){ unique[ JSON.stringify(a) ] = 1 }) ...
- 去除List集合中的重复对象,Map遍历代码
/*** * 去除List<PartsInfoDTO>列表中的重复对象 ~!! * @param list * @return */ public static List<Parts ...
- 给json对象去除重复的值
给数组去除重复值 Array.prototype.distinct = function() { var arr = this, result = [], i, j, len = arr.length ...
- [UE4]复制引起的重复对象
一.在角色的BeginPlay事件中,在角色正前方1米到2米处生成一立方体. 二.开启2个玩家,第一个创建是服务器端,第二个窗口是客户端.可以看到:服务器端窗口创建了2个灰色的立方体,客户端却创建了4 ...
随机推荐
- 【FAQ】接入华为应用内支付服务常见问题解答
HMS Core应用内支付服务(In-App Purchases,IAP)为应用提供便捷的应用内支付体验和简便的接入流程.开发者的应用集成IAP SDK后,调用IAP SDK接口,启动IAP收银台,即 ...
- JAVA中让Swagger产出更加符合我们诉求的描述文档,按需决定显示或者隐藏指定内容
大家好,又见面啦. 在前一篇文档<JAVA中自定义扩展Swagger的能力,自动生成参数取值含义说明,提升开发效率>中,我们探讨了如何通过自定义注解的方式扩展swagger的能力让Swag ...
- 利用京东云Web应用防火墙实现Web入侵防护
摘 要 本指南描述如何利用京东云Web应用防火墙(简称WAF),对一个简单的网站(无论运行在京东云.其它公有云或者IDC)进行Web完全防护的全过程.该指南包括如下内容: 1 准备环境 1.1 在京东 ...
- 使用PBIS将Linux加入域
使用PBIS将Linux加入域 很多企业已经部署的微软的活动目录,为了方便管理,可以把Linux加入域.网上流传了很多把Linux加入域的方法,感觉比较复杂,并且似乎并没有真正的加入域.只是完成 ...
- C# 常见面试问题
C# 常见面试问题 EntityFramework 数据持久化 C#中的委托是什么?事件是不是一种委托? C#中的委托是一种引用类型,表示具有相同方法签名的方法的引用.类似于函数指针,也就是说它们是指 ...
- 基于MySQL的-u选项实现如何最大程度防止人为误操作MySQL数据库
在mysql命令加上选项-U后,当发出没有WHERE或LIMIT关键字的UPDATE或DELETE时,MySQL程序就会拒绝执行.那么,我们基于MySQL提供的这项设置,就可以轻松实现如何最大程度防止 ...
- C#高级特性(反射)
今天来讲解反射的应用: 一.反射是什么? 简诉一下,反射就是.Net Framework 的一个帮助类库,可以获取并使用metadata(元数据清单):说的通俗易懂点,就是不用通过引用,仍然可以使用其 ...
- C#并发编程-4 同步
如果程序用到了并发技术,那就要特别留意这种情况:一段代码需要修改数据,同时其他代码需要访问同一个数据. 这种情况就需要考虑同步地访问数据. 如果下面三个条件都满足,就必须用同步来保护共享的数据. 多段 ...
- <三>从编译器角度理解C++代码编译和链接原理
1代码 点击查看代码 **sum.cpp** int gdata=10; int sum(int a,int b){ return a+b; } **main.cpp** extern int gda ...
- 使用idea操作git(ssh协议)
问题 我们发现,使用IDEA上的git功能,当使用ssh协议出现了可以commit但无法push和pull的问题,经过测试发现原因是Could not read from remsitory.直接翻译 ...