1. 简介

本文基于JDK8u111的源码分析WeakHashMap的一些主要方法的实现。

2. 数据结构

就数据结构来说WeakHashMap与HashMap原理差不多,都是拉链法来解决哈希冲突。

下面是WeakHashMap中的Entry结构定义。

/**
* 省略部分方法实现。
*/
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;
}
@SuppressWarnings("unchecked")
public K getKey() {
return (K) WeakHashMap.unmaskNull(get());
}
public V getValue() {
return value;
} public V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} }

另外,每个WeakHashMap内部都有一个ReferenceQueue用于收集被GC的弱引用,定义如下。

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

这个queue会作为Entry构造方法的一个参数用于实例化WeakReference,其主要作用是为方便清理WeakHashMap中无效Entry。

3. 重要方法源码

3.1 哈希算法

首先看一下WeakHashMap是如何去hash一个Object的

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

可以看到WeakHashMap的hash方法实际上是和JDK7中HashMap是相同的。

因为WeakHashMap与HashMap类似,Capacity也是2的幂。如果直接用对象的hashCode那么在计算bucket的index的时候可能会出现比较严重的冲突(高位不同,低位相同分配到同一个bucket中)。为了避免这种情况,需要将高位与低位作一个混淆或者扰动,增加bucket index的随机性。

在JDK8的HashMap类中,hash方法已经简化为只需要一次扰动亦或。

3.2 插入元素

public V put(K key, V value) {
Object k = maskNull(key);
int h = hash(k); // getTable会作一次清理。
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length); // 遍历bucket中元素,查询是否命中map中已有元素。
for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
if (h == e.hash && eq(k, e.get())) {
V oldValue = e.value;
if (value != oldValue)
e.value = value;
return oldValue;
}
} modCount++;
Entry<K,V> e = tab[i];
// 将新元素插入到bucket中。
tab[i] = new Entry<>(k, value, queue, h, e); // 超过阈值后扩容一倍。
if (++size >= threshold)
resize(tab.length * 2);
return null;
}
private Entry<K,V>[] getTable() {
expungeStaleEntries();
return table;
}

下面来看看WeakHashMap是如何清理脏数据的

private void expungeStaleEntries() {
// 遍历该WeakHashMap的reference queue中被回收的弱引用。
for (Object x; (x = queue.poll()) != null; ) {
/*
* 这里有个值得注意的点就是下面的代码被包在queue的同步块中。
* 因为这里不同步的话,WeakHashMap在不涉及修改,只有并发读的情况下,
* 下面的清理在多线程情况下可能会破坏内部数据结构。
* 而之所以不在整个方法级别作同步,原因是上面的ReferenceQueue的poll方法是线程安全,
* 可以并发取数据的(poll方法里面有同步)。
*/
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;
// 遍历对应bucket中的元素。
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
// 意味着table[i]==e,直接将table[i]向后指一位即可
if (prev == e)
table[i] = next;
else
// 删除p节点,将前驱和后继链接上。
prev.next = next;
// 因为可能有HashIterator正在遍历,所以e.next这里不清为null。
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

3.2.1 扩容机制

与HashMap类似,在WeakHashMap中元素超过阈值threshold时也会发生扩容,下面是WeakHashMap的resize方法实现

void resize(int newCapacity) {
Entry<K,V>[] oldTable = getTable();
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry<K,V>[] newTable = newTable(newCapacity);
transfer(oldTable, newTable);
table = newTable; /*
* WeakHashMap这里有个很严谨的设计是会再次判断元素个数是否超过阈值的一半
* 因为在刚开始getTable以及后续transfer过程中都有清理机制(transfer方法不会去拷贝已经被回收的元素)。
* 如果size的值小于阈值的一半,为了避免WeakHashMap的Capacity的无限扩张,会去重新将元素拷贝到原先的数组中。
*/
if (size >= threshold / 2) {
threshold = (int)(newCapacity * loadFactor);
} else {
expungeStaleEntries();
transfer(newTable, oldTable);
table = oldTable;
}
} private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
for (int j = 0; j < src.length; ++j) {
Entry<K,V> e = src[j];
src[j] = null;
while (e != null) {
Entry<K,V> next = e.next;
Object key = e.get();
if (key == null) {
e.next = null;
e.value = null;
size--;
} else {
int i = indexFor(e.hash, dest.length);
e.next = dest[i];
dest[i] = e;
}
e = next;
}
}
}

至此,put方法我们已经阅读的差不多了。这里梳理一下WeakHashMap在我们操作put元素时哪些情况下会清理元素

  • put方法开始的getTable会调用一次expungeStaleEntries
  • 需要扩容时resize方法开始的getTable会调用一次expungeStaleEntries
  • transfer方法本身会判断弱引用指向的对象是否已经被GC
  • 扩容后发现size小于阈值一半,会调用一次expungeStaleEntries

3.3 取出元素

WeakHashMap根据key获取一个mapping对应的value还是相对比较简单的。

public V get(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int index = indexFor(h, tab.length);
Entry<K,V> e = tab[index];
// 遍历bucket中元素。
while (e != null) {
if (e.hash == h && eq(k, e.get()))
return e.value;
e = e.next;
}
return null;
}

可以看到在get方法中也有getTable方法的调用,这里也会涉及到已被GC的key对应entry的清理。

3.3 删除元素

public V remove(Object key) {
Object k = maskNull(key);
int h = hash(k);
Entry<K,V>[] tab = getTable();
int i = indexFor(h, tab.length);
Entry<K,V> prev = tab[i];
Entry<K,V> e = prev; while (e != null) {
Entry<K,V> next = e.next;
/*
* 这里的逻辑其实和expungeStaleEntries类似,
* 如果在bucket最外的端点,则直接把tab[i]的指向往后面挪一下即可,
* 否则将待删除节点前驱和后继链接上即可。
*/
if (h == e.hash && eq(k, e.get())) {
modCount++;
size--;
if (prev == e)
tab[i] = next;
else
prev.next = next;
return e.value;
}
prev = e;
e = next;
} return null;
}

4. WeakHashMap的使用

WeakHashMap的一种使用场景是不影响key生命周期的缓存。可以参考tomcat中的ConcurrentCache中,使用了WeakHashMap。我们来看下代码:

public final class ConcurrentCache<K,V> {

    private final int size;

    private final Map<K,V> eden;

    private final Map<K,V> longterm;

    public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
} public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
synchronized (longterm) {
v = this.longterm.get(k);
}
if (v != null) {
this.eden.put(k, v);
}
}
return v;
} public void put(K k, V v) {
if (this.eden.size() >= size) {
synchronized (longterm) {
this.longterm.putAll(this.eden);
}
this.eden.clear();
}
this.eden.put(k, v);
}
}

不过在我实际项目开发中,一般碰到需要用到WeakHashMap的场景还是比较少见的。

5. 总结

下面总结一下WeakHashMap,并和HashMap以及ThreadLocalMap作一个比较。

比较内容 WeakHashMap HashMap ThreadLocalMap
存储方式 拉链法 拉链法 开放地址法
存储数据 任意,key/value可为null,实际存储的Entry为key的弱引用。 任意,key/value可为null key为ThreadLocal对象,value任意类型可为null,key一定不会为null(没法自己塞null),实际存储的Entry是key的弱引用。
对key的GC影响 Entry为弱引用,不影响key的GC 强引用,对key的GC有影响 Entry为弱引用,不影响key的GC
线程安全
其它 自带无效数据清理 JDK8中方法实现有优化 自带无效数据清理

WeakHashMap源码解读的更多相关文章

  1. WeakHashMap,源码解读

    概述 WeakHashMap也是Map接口的一个实现类,它与HashMap相似,也是一个哈希表,存储key-value pair,而且也是非线程安全的.不过WeakHashMap并没有引入红黑树来尽量 ...

  2. jdk1.8.0_45源码解读——Map接口和AbstractMap抽象类的实现

    jdk1.8.0_45源码解读——Map接口和AbstractMap抽象类的实现 一. Map架构 如上图:(01) Map 是映射接口,Map中存储的内容是键值对(key-value).(02) A ...

  3. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  4. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  5. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  6. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  7. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  8. SDWebImage源码解读之SDWebImageCache(上)

    第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...

  9. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

随机推荐

  1. mac在命令行中打开某个文件夹

    使用 open 命令,如打开 ~/Download/abc open ~/Download/abc

  2. Ubuntu14.04 编译 Android 5.1.1源码(采用国内镜像)

    欢迎转载,转载请注明出处: http://www.cnblogs.com/lanrenxinxin/p/5424554.html 之前就有编译android源码的想法,但是由于有GFW的存在,又没有梯 ...

  3. 自己动手实现java数据结构(三) 栈

    1.栈的介绍 在许多算法设计中都需要一种"先进后出(First Input Last Output)"的数据结构,因而一种被称为"栈"的数据结构被抽象了出来. ...

  4. Java设计模式学习记录-解释器模式

    前言 这次介绍另一个行为模式,解释器模式,都说解释器模式用的少,其实只是我们在日常的开发中用的少,但是一些开源框架中还是能见到它的影子,例如:spring的spEL表达式在解析时就用到了解释器模式,以 ...

  5. 细说Redis(二)之 Redis的持久化

    前言 在上一篇文章[细说Redis(一)之 Redis的数据结构与应用场景]中,主要介绍了Reids的数据结构. 对于redis的执行命令,这里不做介绍,因为网上搜索一堆,无必要再做介绍. AOF&a ...

  6. SQLServer转PowerDesigner(转载)

    将SQLServer中的数据库导入到PowerDesigner 百度得: https://jingyan.baidu.com/article/20095761c98042cb0721b4fc.html ...

  7. fllume 入门

    flume flume 简介及核心概念 什么是flume Flume是Cloudera提供的一个高可用的,高可靠的,分布式的海量日志采集.聚合和传输的系统,目前是Apache的顶级项目.Flume支持 ...

  8. ELK日志分析平台系统windows环境搭建和基本使用

    ELK(ElasticSearch, Logstash, Kibana),三者组合在一起就可以搭建实时的日志分析平台啦! Logstash主要用来收集.过滤日志信息并将其存储,所以主要用来提供信息. ...

  9. 前端的业余设计-about my 毕业季

    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ...

  10. docker 安装jenkins

    基于docker 进行安装 软件,首先需要有docker环境. 1.docker 下载 jenkins 镜像 指定版本 ,因为低版本的后面安装 软件会失败(亲测). docker pull jenki ...