本文主要简要分析了Java中和Redis中HashMap的实现,并且对比了两者的异同

1.Java的实现

下图表示了Java中一个HashMap的主要实现方式

因为大家对于Java中HashMap的实现方式,已经比较熟悉了,所以咱们只是简单的说一下.

基本结构

table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。 size是HashMap的大小,它是HashMap保存的键值对的数量。 threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。loadFactor就是加载因子。 modCount是用来实现fail-fast机制的。

计算Hash值和在数组中的位置
//length为Entry数组长度
static int indexFor(int h, int length) { 
return h & (length - 1);
}
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
添加键值对时的操作(put)
// 将“key-value”添加到HashMap中
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。 
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。 
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出! 
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;  e.value = value; 
e.recordAccess(this); return oldValue; 
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中 
modCount++; 
addEntry(hash, key, value, i); return null;}
解决Hash冲突的方式,扩容时机
// 新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) { 
// 保存“bucketIndex”位置的值到“e”中 
Entry<K,V> e = table[bucketIndex]; 
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点” 
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
// 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小 
if (size++ >= threshold)resize(2 * table.length);
}
扩容过程
// 重新调整HashMap的大小,newCapacity是调整后的单位
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return; 
}
// 新建一个HashMap,将“旧HashMap”的全部元素添加到“新HashMap”中,
// 然后,将“新HashMap”赋值给“旧HashMap”。 
Entry[] newTable = new Entry[newCapacity]; 
transfer(newTable); 
table = newTable; 
threshold = (int)(newCapacity * loadFactor);
}
// 将HashMap中的全部元素都添加到newTable中
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); 
e.next = newTable[i]; 
newTable[i] = e;  
e = next; 
} while (e != null); 
}
}
}

2.Redis的实现

整个基本结构

哈希表
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小(相当于Java中的capacity)
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量(相当于Java中的size)
unsigned long used;
} dictht;
键值对
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
哈希结构
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

简单例子:

添加键值对时的操作(dictAdd)

计算 hash 和 数组位置
# 使用字典设置的哈希函数,计算键 key 的哈希值(相当于hash())
hash = dict->type->hashFunction(key);
# 使用哈希表的 sizemask 属性和哈希值,计算出索引值(相当于indexFor())
index = hash & dict->ht[x].sizemask;

先计算 key的哈希值 在将 该哈希值&(数组长度-1)确定下标(与Java极为相似)

注:Redis 使用 MurmurHash2 算法来计算键的哈希值;这种算法的优点在于, 即使输入的键是有规律的, 算法仍能给出一个很好的随机分布性, 并且算法的计算速度也非常快。关于 MurmurHash 算法的更多信息可以参考该算法的主页: http://code.google.com/p/smhasher/ 。

解决hash冲突

用拉链法解决hash冲突,将旧entry链表插进新entry尾部(与Java极为相似)

扩容时机

当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:

  1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
  2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5;

注:该值不可通过配置来修改,要变必须改源码。

其中哈希表的负载因子可以通过公式:

# 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

该值和Java不同,Java默认值为0.75,相比之下Java扩容更加积极。

根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。

缩容时机(Java中不会自动缩容)

当哈希表的负载因子小于 0.1

 时, 程序自动开始对哈希表执行收缩操作

因为Java中HashMap不会自动缩容,所以在在大量put后,再大量remove,并且还持有该引用的话,会浪费很多内存

变容过程

渐进式转移

扩展或收缩哈希表需要将 ht[0]里面的所有键值对 rehash 到 ht[1]里面, 但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。

这样做的原因在于, 如果 ht[0]里只保存着四个键值对, 那么服务器可以在瞬间就将这些键值对全部 rehash 到 ht[1]; 但是, 如果哈希表里保存的键值对数量不是四个, 而是四百万、四千万甚至四亿个键值对, 那么要一次性将这些键值对全部 rehash 到 ht[1]的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。

因此, 为了避免 rehash 对服务器性能造成影响, 服务器不是一次性将 ht[0]里面的所有键值对全部 rehash 到 ht[1], 而是分多次、渐进式地将 ht[0]里面的键值对慢慢地 rehash 到 ht[1]。

因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0]和 ht[1]两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在ht[0]里面进行查找, 如果没找到的话, 就会继续到 ht[1]里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1]里面, 而 ht[0]则不再进行任何添加操作: 这一措施保证了 ht[0]包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

3.对比两者的异同

Java Redis

基本结构

两者都是键值对数组,键值对是链表

计算哈希值和数组位置

通过自身hash函数,计算hash值,与数组长度&,确定数组下标

解决Hash冲突的方式

拉链法

容量变化时机

默认值为0.75,更加消极,有缩容 默认值为1,更加积极,只可变大,不可变小

容量变化过程

一起完成 分次完成(渐进式)

HashMap的两种实现方式的更多相关文章

  1. HashMap的两种遍历方式

    HashMap的两种遍历方式 HashMap存储的是键值对:key-value . java将HashMap的键值对作为一个整体对象(java.util.Map.Entry)进行处理,这优化了Hash ...

  2. [Java]HashMap的两种排序方式

    先将 Map 中的 key 和 value 全部取出来封装成 JavaBea 数组,再将这个数组排序,排序完成后,重新写回 Map 中,写回时采用 LinkedHashMap 可以保证迭代的顺序. 下 ...

  3. HashMap的两种排序方式

    Map<String, Integer> map = new HashMap<String, Integer>();map.put("d", 2);map. ...

  4. Map集合的两种遍历方式

    Map集合:即 接口Map<K,V> map集合的两种取出方式:    1.Set<k> keyset: 将map中所有的键存入到set集合(即将所有的key值存入到set中) ...

  5. 细说java中Map的两种迭代方式

    曾经对java中迭代方式总是迷迷糊糊的,今天总算弄懂了.特意的总结了一下.基本是算是理解透彻了. 1.再说Map之前先说下Iterator: Iterator主要用于遍历(即迭代訪问)Collecti ...

  6. Map集合的两种取出方式

    Map集合有两种取出方式, 1.keySet:将Map中的键存入Set集合,利用set的迭代器来处理所有的键 举例代码如下: import java.util.*; class Test { publ ...

  7. Map的两种遍历方式

    ********************************************************************************* ****************** ...

  8. Java中的HashMap的2种遍历方式比较

    首先我们准备数据,准备一个map Map<String, String> map = new HashMap<String, String>(); for (int i = 0 ...

  9. 谨慎使用keySet:对于HashMap的2种遍历方式比较

    HashMap存储的是键值对,所以一般情况下其遍历同List及Set应该有所不同. 但java巧妙的将HashMap的键值对作为一个整体对象(java.util.Map.Entry)进行处理,这优化了 ...

随机推荐

  1. Python基础篇【第5篇】: Python模块基础(一)

    模块 简介 在计算机程序的开发过程中,随着程序代码越写越多,在一个文件里代码就会越来越长,越来越不容易维护. 为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就 ...

  2. jQuery MiniUI开发系列之:安装部署

    jQuery MiniUI是一套纯Javascript的WebUI控件库,它由几十个Javascript控件组成,是不依赖服务端和数据库的. 下载jQuery MiniUI,解压缩后,开发者可以直接在 ...

  3. PHP常用功能

    1.PHP字符串 字符串声明 变量=''或者""(一般情况会使用单引号,因为写起来会比较方便) $str = 'Hello PHP'; echo $str; strpos 计算字符 ...

  4. Java内存浅析分类

    当jvm运行起来的时候,它会向系统申请一片内存区(不同的jvm实现可能不同,有些可以使用虚拟内存),并将这块内存分出一部分存储程序创建的对象,传递给方法的参数,返回值,局部变量等等,我们将这块内存称之 ...

  5. 第一章 Andorid系统移植与驱动开发概述 - 读书笔记

    Android驱动月考1 第一章 Andorid系统移植与驱动开发概述 - 读书笔记 1.Android系统的架构: (1)Linux内核,Android是基于Linux内核的操作系统,并且开源,所以 ...

  6. xampp3.2下mysql中文乱码终极解决方案

    xmapp3.2.2中mysql已经被替换成了Mariadb,网上那些显示char语句已经失灵. 另外本文主要介绍的是手动在mysql中写入中文乱码问题 那么我们将采用如下三个步骤解决乱码问题 1.打 ...

  7. ubuntu-利用pdnsd-TCP方式获取IP-拒绝DNS污染

    那,自从国内技术出现了DNS污染问题呢,时常导致很多国外网站访问不正常,所以通过参考一些博客所属避免DNS污染的方法,决定搭建一个Ubuntu JeOS下的DNS缓存服务器,该服务器利用TCP方式获取 ...

  8. windows下sass安装 以及一些要注意的问题

    都说sass 环境难配其时也没那么难 按照以下步骤一下一下来还是挺快的 如果你是喜欢less 那就当我没说 233333 1.sass 是基于ruby这门语言的需要使用 rubygem这个包管理器安装 ...

  9. Android 自定义表格显示数据

    Android 自定义TextView控件,用来组成表格方便数据的展示. 首先看一下效果 样式不是很好看,需要用的可以自己优化一下. 实现方式很简单. 1.自定义控件 MyTableTextView ...

  10. 关于Hibernate XXX is not mapped 错误

    我的实体类是这么配置的 @Entity(name="EntityName")  //必须,name为可选,对应数据库中一的个表 就会出现 XXX is not mapped.   ...