JDK源码分析(9)之 WeakHashMap 相关

平时我们使用最多的数据结构肯定是 HashMap,但是在使用的时候我们必须知道每个键值对的生命周期,并且手动清除它;但是如果我们不是很清楚它的生命周期,这时候就比较麻烦;通常有这样几种处理方式:

  • 由一个线程定时处理,可以是Timer或者ScheduledThreadPoolExecutor
  • 利用重写LinkedHashMap.removeEldestEntry(),实现 FIFOCache 或者 LRUCache;可以参考我之前写的一篇博客 LinkedHashMap 相关
  • 利用 WeakHashMap 的特性,如果逻辑比较复杂还可以直接使用Reference;这里可以参考 Reference 完全解读Reference 框架概览

所以本文将主要介绍WeakHashMap的特性,以及补充一些关于 HashMap 实现的对比;相关 HashMap 的介绍也可以参考 HashMap 相关

一、使用场景

上面也介绍了,WeakHashMap适用于不是非常重要的缓存类似的场景;例如:

WeakHashMap<Object, Integer> map = new WeakHashMap<>();

for (int i = 0; i < 100; i++) {
  map.put(new Object(), i);
}

System.out.println(map.size());  // 1
System.gc();                     // 2
System.out.println(map.size());  // 3
System.out.println(map.size());  // 4
System.out.println(map.size());  // 5
System.out.println(map);         // 6
System.out.println(map.size());  // 7

// 打印:
100
100
100
46
{}
0

对于以上的结果你可能和我打印的不一样,WeakHashMap按照语义应该是,当 key 没有强引用指向的时候,会自动清除 key 和 value;我这里先解释它的释放过程,如果你觉得很清晰,那WeakHashMap你就算是掌握了;

  • 首先 for 循环结束的时候,key 已经没用强引用指向了,此时所有的 key 都是弱引用了;
  • 接下来执行1,因为我这里只有一个方法,新生代还有足够的空间,所以不会触发 GC,所以所有的 key 任然在堆里面,所以打印100;
  • 然后手动触发 GC,虽然System.gc();不一定会立即执行,但是我这里只有一个方法,所以肯定会执行 GC,这里可以打开 GC 日志查看,-verbose:gc;因为 所有的 key 都是弱引用,所以referent被致为 null,同时将 key 注册到 ReferenceQueue中;
  • 在执行 3-7 的时候,按语义 map 应该为空;但是将 key 注册到 ReferenceQueue并非原子性一次完成的,所以这里会打印不同的值,每注册完成一个,在 map 进行操作的时候,就会将其移除;

将上面的代码改成多线程分析思路也是一样的,如果你觉得有不清楚的地方可以查看下文;

二、WeakHashMap 源码分析

1. 类定义

public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>

可以看到虽然WeakHashMap也是基于哈希表,但是却并非像LinkedHashMap一样是继承于HashMap,并且WeakHashMap也没有实现Cloneable, Serializable两个接口,这是因为WeakHashMap基于WeakReference实现的,弱引用并不建议实现序列化,同时弱引用一般用于不是很重要的缓存,也就没必要实现Cloneable, Serializable两个接口了;

2. 核心方法

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
  V value;
  final int hash;
  Entry<K,V> next;

  Entry(Object key, V value, ReferenceQueue<Object> queue, int hash, Entry<K,V> next) {
    super(key, queue);
    this.value = value;
    this.hash  = hash;
    this.next  = next;
  }

  public K getKey() { }
  public V getValue() {
  public V setValue(V newValue) {
  public int hashCode() {
  public String toString() {
}

private void expungeStaleEntries() {
  for (Object x; (x = queue.poll()) != null; ) {
    synchronized (queue) {
      @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>) x;
      int i = indexFor(e.hash, table.length);

      Entry<K,V> prev = table[i];
      Entry<K,V> p = prev;
      while (p != null) {
        Entry<K,V> next = p.next;
        if (p == e) {
          if (prev == e)
            table[i] = next;
          else
            prev.next = next;
          // Must not null out e.next;
          // stale entries may be in use by a HashIterator
          e.value = null; // Help GC
          size--;
          break;
        }
        prev = p;
        p = next;
      }
    }
  }
}

上面代码所列的ReferenceQueue,Entry,expungeStaleEntries()就是WeakHashMap实现的核心了;这里强烈建议要先看 Reference 完全解读Reference 框架概览 这两篇博客,里面同样的内容我也不会再赘述了;

  • Entry<K,V> extends WeakReference<Object>, 表明所有的节点都是WeakReference,而 key 则是 referent;
  • queue,所有 key 使用同一个ReferenceQueue监听器,每当 key 被回收的时候,entry 将会被注册到ReferenceQueue中;
  • expungeStaleEntries,将注册到ReferenceQueue中的 entry 移除,并将 value 置为 null;WeakHashMap的所有操作都先执行expungeStaleEntries,这样WeakHashMap就实现了自动回收不在需要的 key 和 value;

三、性能对比

其实上面的内容就已经将WeakHashMap的主要实现讲完了,但是我之前在看HashMap源码的时候,并没有对比 JDK1.7 和 JDK1.8,但是在这里发现其实WeakHashMap的实现和 JDK1.7 差不多,所以接下来我将主要对比一下WeakHashMapHashMap

1. 容量计算

WeakHashMapHashMap中都要求容量是2的幂,因为当容量为2的幂时,使用除留余数法计算哈希桶位置时可以使用hash % length = hash & (length-1)的性质进行优化;

// WeakHashMap
int capacity = 1;
while (capacity < initialCapacity)
  capacity <<= 1;

// HashMap
static final int tableSizeFor(int cap) {
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

简单测试可以得到:

initCap = 10 50 100
WeakHashMap 30 32 26
HashMap 3 3 3

代码比较简单我就不贴了,从上表也可以看到了tableSizeFor不仅高效而且稳定;

2. 哈希计算

// WeakHashMap
final int hash(Object k) {
  int h = k.hashCode();
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
}

// HashMap
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

两种hash算法都是要避免极端的hashCode(),但是HashMap却更为透彻,因为影响哈希桶位置的只有 hash 的低位(容量2的n次方,n个低位),直接将高位与上低位,使高位 hash 参与位置计算,简洁且高效;

此外还有put方法,但是里面还牵涉红黑树,对于本文就扯得有点远了,所以暂不讲;

总结

  • WeakHashMapWeakReference的典型应用,在灵活应用WeakHashMap之后,如果有更为复杂的逻辑,可以直接使用Reference实现;

原文地址:https://www.cnblogs.com/sanzao/p/10367123.html

时间: 2024-10-16 19:56:44

JDK源码分析(9)之 WeakHashMap 相关的相关文章

【JDK】JDK源码分析-CountDownLatch

概述 CountDownLatch 是并发包中的一个工具类,它的典型应用场景为:一个线程等待几个线程执行,待这几个线程结束后,该线程再继续执行. 简单起见,可以把它理解为一个倒数的计数器:初始值为线程数,每个线程结束时执行减 1 操作,当计数器减到 0 时等待的线程再继续执行. 代码分析 CountDownLatch 的类签名和主要方法如下: public class CountDownLatch {} 常用方法为:await().await(long, TimeUnit) 和 countDow

【JDK】JDK源码分析-Semaphore

概述 Semaphore 是并发包中的一个工具类,可理解为信号量.通常可以作为限流器使用,即限制访问某个资源的线程个数,比如用于限制连接池的连接数. 打个通俗的比方,可以把 Semaphore 理解为一辆公交车:车上的座位数(初始的“许可” permits 数量)是固定的,行驶期间如果有人上车(获取许可),座位数(许可数量)就会减少,当人满的时候不能再继续上车了(获取许可失败):而有人下车(释放许可)后就空出了一些座位,其他人就可以继续上车了. 下面具体分析其代码实现. 代码分析 Semapho

JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlockingQueue,以便日后灵活使用. 1. 在Java的Concurrent包中,添加了阻塞队列BlockingQueue,用于多线程编程.BlockingQueue的核心方法有: boolean add(E e) ,把 e 添加到BlockingQueue里.如果BlockingQueue可以容纳,则返回true,否则抛出异常. boolean offer(E e),表示如果可能的话,将 e 加到B

JDK源码分析之String篇

------------------------------String在内存中的存储情况(一下内容摘自参考资料1)----------------------------------- 前提:先了解下什么是声明,什么时候才算是产生了对象实例 其中x并未看到内存分配,变量在使用前必须先声明,再赋值,然后才可以使用.java基础数据类型会用对应的默认值进行初始化 一.首先看看Java虚拟机JVM的内存块及其变量.对象内存空间是怎么存储分配的 1.栈:存放基本数据类型及对象变量的引用,对象本身不存放

【JDK源码分析】通过源码分析CyclicBarrier

前言 CyclicBarrier它是什么?一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点.类似于朋友之间联系要在中午聚个会,几个朋友全部到齐后才开始喝酒吃菜. 源码 CyclicBarrier属性和构造器 public class CyclicBarrier { // 互斥锁 private final ReentrantLock lock = new ReentrantLock(); // 条件等待 private final Condition trip = lock.new

JDK源码分析之concurrent包(三) -- Future方式的实现

上一篇我们基于JDK的源码对线程池ThreadPoolExecutor的实现做了分析,本篇来对Executor框架中另一种典型用法Future方式做源码解读.我们知道Future方式实现了带有返回值的程序的异步调用,关于异步调用的场景大家可以自行脑补Ajax的应用(获取返回结果的方式不同,Future是主动询问获取,Ajax是回调函数),这里不做过多说明. 在进入源码前,首先来看下Future方式相关的API: 接口Callable:有返回结果并且可能抛出异常的任务: 接口Future:表示异步

jdk源码分析总览

今天看到了一个源码分析按照重要性排序的例子, 这里拿过来用了,之后按照这个顺序不断的完善源码的内容. 引用的出处忘记了(对作者说声抱歉) 很多java开发的小伙伴都会阅读jdk源码,然而确不知道应该从哪读起.以下为小编整理的通常所需阅读的源码范围. 标题为包名,后面序号为优先级1-4,优先级递减 1.java.lang 1) Object 12) String 13) AbstractStringBuilder 14) StringBuffer 15) StringBuilder 16) Boo

JDK源码分析-AtomicInteger

AtomicInteger可以看做Integer类的原子操作工具类.在java.util.concurrent.atomic包下,在一些使用场合下可以取代加锁操作提高并发性.接下来就从几个方面来介绍: 1.原子性和CAS. 2.CPU底层实现原理. 3.atomic包介绍. 4.源码分析. 原子性和CAS 原子性就是指某一个操作是不可拆分的,是一个整体必须要一次性全部执行完成要么就不执行. CAS是Compare And Swap(比较并交换).意思是当你要更新某个值的时候先要检查这个变量的当前

JDK源码分析-Integer

Integer是平时开发中最常用的类之一,但是如果没有研究过源码很多特性和坑可能就不知道,下面深入源码来分析一下Integer的设计和实现. Integer: 继承结构: -java.lang.Object --java.lang.Number ---java.lang.Integer 其中父类Number是个抽象类,是所有数字类型相关的类的父类,例如Double.Float.Integer.Long 和 Short. Integer类还实现了Comparable接口用以比较两个Integer的