• 支持 equals 相等的对象(可重复对象)作为 WeakHashMap 的 Key


    问题

    长链接场景下通常有一个类似 Map> 的结构,用来查找一个逻辑组内的哪些用户,String 类型的 Entry.key 是逻辑组 key,Set 类型的 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,但这个 Map 同样不能内存泄漏,不过情况略有不同,区别在于查找 Set 变成了一个嵌套的查找(String -> Object -> Set),而用户强引用的 Entry.key 变成了 Object,即 Object 对象的生命周期跟随用户走即可( WeakHashMap> 负责 GC Set),也就是 WeakHashMap>。

    解决

    下面给出代码:

    1. package io.github.hligaty.util;
    2. import java.lang.ref.WeakReference;
    3. import java.util.Objects;
    4. import java.util.WeakHashMap;
    5. import java.util.concurrent.locks.ReadWriteLock;
    6. import java.util.concurrent.locks.ReentrantReadWriteLock;
    7. /**
    8. * Recreatable key objects.
    9. * With recreatable key objects,
    10. * the automatic removal of WeakHashMap entries whose keys have been discarded may prove to be confusing,
    11. * but WeakKey will not.
    12. *
    13. * @param the type of keys maintained
    14. * @author hligaty
    15. * @see java.util.WeakHashMap
    16. */
    17. public class WeakKey<K> {
    18. private static final WeakHashMap<WeakKey<?>, WeakReference<WeakKey<?>>> cache = new WeakHashMap<>();
    19. private static final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
    20. private static final WeakHashMap<Thread, WeakKey<?>> shadowCache = new WeakHashMap<>();
    21. private static final ReadWriteLock shadowCacheLock = new ReentrantReadWriteLock();
    22. private K key;
    23. private WeakKey() {
    24. }
    25. @SuppressWarnings("unchecked")
    26. public static <T> WeakKey<T> wrap(T key) {
    27. WeakKey<T> shadow = (WeakKey<T>) getShadow();
    28. shadow.key = key;
    29. cacheLock.readLock().lock();
    30. try {
    31. WeakReference<WeakKey<?>> ref = cache.get(shadow);
    32. if (ref != null) {
    33. shadow.key = null;
    34. return (WeakKey<T>) ref.get();
    35. }
    36. } finally {
    37. cacheLock.readLock().unlock();
    38. }
    39. cacheLock.writeLock().lock();
    40. try {
    41. WeakReference<WeakKey<?>> newRef = cache.get(shadow);
    42. shadow.key = null;
    43. if (newRef == null) {
    44. WeakKey<T> weakKey = new WeakKey<>();
    45. weakKey.key = key;
    46. newRef = new WeakReference<>(weakKey);
    47. cache.put(weakKey, newRef);
    48. return weakKey;
    49. }
    50. return (WeakKey<T>) newRef.get();
    51. } finally {
    52. cacheLock.writeLock().unlock();
    53. }
    54. }
    55. private static WeakKey<?> getShadow() {
    56. Thread thread = Thread.currentThread();
    57. shadowCacheLock.readLock().lock();
    58. WeakKey<?> shadow;
    59. try {
    60. shadow = shadowCache.get(thread);
    61. if (shadow != null) {
    62. return shadow;
    63. }
    64. } finally {
    65. shadowCacheLock.readLock().unlock();
    66. }
    67. shadowCacheLock.writeLock().lock();
    68. try {
    69. shadow = shadowCache.get(thread);
    70. if (shadow == null) {
    71. shadow = new WeakKey<>();
    72. shadowCache.put(thread, shadow);
    73. return shadow;
    74. }
    75. return shadow;
    76. } finally {
    77. shadowCacheLock.writeLock().unlock();
    78. }
    79. }
    80. public K unwrap() {
    81. return key;
    82. }
    83. @Override
    84. public boolean equals(Object o) {
    85. if (this == o) return true;
    86. if (o == null || getClass() != o.getClass()) return false;
    87. WeakKey<?> weakKey = (WeakKey<?>) o;
    88. return Objects.equals(key, weakKey.key);
    89. }
    90. @Override
    91. public int hashCode() {
    92. return Objects.hash(key);
    93. }
    94. @Override
    95. public String toString() {
    96. return "WeakKey{" +
    97. "attr=" + key +
    98. '}';
    99. }
    100. }

    WeakKey 是前面说的 Object,使用时将需要释放的数据 Data 放到以 WeakKey 为 key 的 WeakHashMap(WeakHashMap),这样当全部用户释放 WeakKey 引用时就可以完成 WeakHashMap Entry 的 GC(包括 WeakKey 和 Data)。

    WeakKey 的主要工作是将用户传入的 key 封装一下再返回,保证全局唯一和内存安全,核心结构是 WeakHashMap, WeakReference>> cache,Entry.key 是对用户 key 封装的 WeakKey,Entry.value 是 Entry.key 外层嵌套的 WeakReference,作用是避免 value 对 key 强引用而无法对 Entry GC。因此 cache 只要没人强引用里面的 WeakKey,这个 map 在 GC 后就是空的,这样就完成了目标,其余的就是优化了。

    如果想在 cache 里查到 WeakKey,那么首先要新建一个 WeakKey,再把 key 赋值到 WeakKey 中,再通过这个新建的 WeakKey 查找,像下面一样:

    1. String key = "key";
    2. WeakKey<String> weakKey = new WeakKey<>();
    3. weakKey.key = key;
    4. WeakReference<WeakKey<?>> ref = cache.get(weakKey);

    每次查找都新建对象,有点沙雕,这里使用缓存对象赋值再查找就可以,另外要保证线程安全,threadLocal 没大问题(ThreadLocal.withInitial(WeakKey::new)),只是不能在 finally 里 remove(remove 的话下次还得新建),在线程池里使用问题不大,不过还有另一种办法,就是 WeakHashMap>,它可以保证这个缓存中的“影子”对象在这个线程只创建一次,当线程被 GC 的同时删除“影子”对象,与 threadLocal 的区别只是牺牲了一些加读锁的时间。

    测试

    下面的 WeakHashMap put 了 Arrays.asList(705, 630, 818) 和 Collections.singletonList(705630818) 两个数据,只有后面的 key 被方法引用了,因此在 GC 后 前一个 key 在 map 中找不到 value,而后一个 key 能获取到 value。

    1. package io.github.hligaty.util;
    2. import org.junit.jupiter.api.Assertions;
    3. import org.junit.jupiter.api.Test;
    4. import java.util.*;
    5. class WeakKeyTest {
    6. @Test
    7. public void testWeakKey() throws InterruptedException {
    8. WeakHashMap<WeakKey<List<Integer>>, Object> map = new WeakHashMap<>();
    9. map.put(WeakKey.wrap(Arrays.asList(705, 630, 818)), new Object());
    10. WeakKey<List<Integer>> weakKey = WeakKey.wrap(Collections.singletonList(705630818));
    11. map.put(weakKey, new Object());
    12. System.gc();
    13. Thread.sleep(5000L);
    14. Assertions.assertNull(map.get(WeakKey.wrap(Arrays.asList(705, 630, 818))));
    15. Assertions.assertNotNull(map.get(WeakKey.wrap(Collections.singletonList(705630818))));
    16. }
    17. }

    其他

    如果你想使用 null,那 WeakKey 是支持的,但需要注意一点,如果你有两个不同类型的 key 使用了 WeakKey,而两者都允许 WeakKey.wrap(null),那么当有一个类型的使用者持有 WeakKey.wrap(null),另一个类型的 WeakKey.wrap(null) 是不会被释放的,因为显然 Objects.equals(null, null) 为 true。

  • 相关阅读:
    Python爬虫入门学习:拿捏高级数据类型
    二叉树最大路径和问题
    stm32单片机个人学习笔记3(GPIO输出)
    实时降噪(Real-time Denoising):Spatio-Temporal Filtering
    分布式(一致性协议)之领导人选举( DotNext.Net.Cluster 实现Raft 选举 )
    使用Typora+EasyBlogImageForTypora写博客,无图床快速上传图片
    Linux单节点安装K8S和kubesphere 已验证安装成功
    二进制编码:“手持两把锟斤拷,口中疾呼烫烫烫”?
    product_00000
    Android 蓝牙 A2dp更改编码Codec格式 (一)
  • 原文地址:https://blog.csdn.net/jh035/article/details/127933003