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. PHP多个进程同时写入同一个文件

    flock (PHP 3 >= 3.0.7, PHP 4, PHP 5) flock -- 轻便的咨询文件锁定 说明 bool flock ( int handle, int operation ...

  2. H5实现魔方游戏

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. Angular Forms - 自定义 ngModel 绑定值的方式

    在 Angular 应用中,我们有两种方式来实现表单绑定--"模板驱动表单"与"响应式表单".这两种方式通常能够很好的处理大部分的情况,但是对于一些特殊的表单控 ...

  4. 【Core】创建简单的Core MVC项目

    创建项目: 首先:打开vs选中新建项目- >选中.NET Core - >ASP.NET Core Web应用程序: 然后:在选择web应用程序,注意上面要选中.net Core 别选错了 ...

  5. 如何靠谱地查到Tomcat的版本

    Tomcat版本获取 一般找jdk的版本的时候,我们直接执行如下命令就可以得知了 java -version 但是Tomcat的版本呢? 除了Tomcat安装目录路径里包含的版本号,还有其他靠谱的获取 ...

  6. cache 访问频率的思考

    互联网的项目用户基数很大,有时候瞬间并发量非常大,这个时候对于数据访问来说是个灾难.为了应对这种场景,一般都会大量采用web服务器集群,缓存集群.采用集群后基本上就能解决大量并发的数据访问.当然这个时 ...

  7. blfs(systemd版本)学习笔记-配置远程访问和管理lfs系统

    我的邮箱地址:zytrenren@163.com欢迎大家交流学习纠错! 要实现远程管理和配置lfs系统需要配置以下软件包: 前几页章节脚本的配置:https://www.cnblogs.com/ren ...

  8. 【读书笔记】iOS-UDID

    UIDevice类可以返回当前iOS设备的UDID,以前开发者通常使用UDID作为识别每台设备的唯一标识,然后从iOS5开始,苹果公司将这一功能标记为废止并不推荐使用,苹果公司在iOS6之后将这个功能 ...

  9. 微信小程序/网站 上传图片到腾讯云COS

    COS简介: 腾讯云提供的一种对象存储服务,供开发者存储海量文件的分布式存储服务.可以将自己开发的应用的存储部分全部接入COS的存储桶中,有效减少应用服务器的带宽,请求等.个人也可以通过腾讯云账号免费 ...

  10. Kotlin入门(7)循环语句的操作

    上一篇文章介绍了简单分支与多路分支的实现,控制语句除了这两种条件分支之外,还有对循环处理的控制,那么本文接下来继续阐述Kotlin如何对循环语句进行操作. Koltin处理循环语句依旧采纳了for和w ...