基于HashMap的散列表性质、原理、优点、性能以及应用研究

在java软件开发过程当中,散列表作为一个存储数据结构的集合,应用当中特别常见,但很多时候只知道它的使用而忽视了其目的、原理,并没有真正挖掘出其真正价值。所以本篇文章从几个角度去深入挖掘散列表的实际价值所在,如有疏漏,敬请指正。

  • 散列表的目的与特性

  • 散列表的原理-HashMap

  • 高性能的map分类与特性分析

  • map性能比较:hashMap、concurrentHashMap在jmh工具下的表现

  • 散列表在投资交易系统中的使用场景

  • 散列表常见错误方式与解决

  • bat面试问题汇总

  • 散列表的边缘地带研究: 二叉树、红黑树、数组结构、链表结构 

散列表目的与特性

在原有的数据结构当中: 经常用到数组和链表;

数组的特点是:寻址容易,插入和删除困难; 而链表的特点是:寻址困难,插入和删除容易。那么有没有一种数据结构能够既寻址容易又能达到插入和删除容易呢,

此时散列表应运而生。 散列表(hashmap)能在查询和修改方便继承了数组的线性查找和链表的寻址修改; HashMap是一种空间还时间的数据结构

散列表的特点是由键值对组合key-value,key-存储值所在的地址,value-真实的数据。 每一个key都有一个对应value,value可以重复,key不可重复

散列表的原理-hashMap

存储方式:拉链法   ,见图1 

 

hashing,哈希算法是通过put(key,value)和get(key)方法来实现数值的位置存储。当需要put值的时候,先调用hash算法,将key解析成固定长度的数字索引hashCode,此索引值就是

value值存储的位置;当需要获取value 时,将key用同样的算法解析hashCode成同样的数字索引,然后获取对应的value

 

碰撞问题:

需要put到同一个bucket(散列桶)中时,当hashCode相同时,那么两个值就会产生碰撞,新的值就会放到同一个bucket当中 ;当get值时,会根据hashCode以及equals比较对象,

去获取实际需要的内容 

当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

 

加载因子: 如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排) 

 

Hashmap的扩容需要满足两个条件:当前数据存储的数量(即size())大小必须大于等于阈值;当前加入的数据是否发生了hash冲突。

(1)、就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。

(2)、当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。

 

HashMap的算法精髓:位运算模式, aspMap的默认初始长度是16,并且每次扩展长度或者手动初始化时,长度必须是2的次幂。通过16位算法让数据分布更加均匀;

通过位运算方式,让效率更高

(参考文章http://www.cnblogs.com/wangdaijun/p/9347541.html)

 

 

高性能的map分类与特性分析

根据map常用特性,可以分为 HashMap、LinkedHashMap、TreeMap、HashTable和ConcurrentHashMap

 

HashMap: 非线程安全的散列表,单线程下put和get效率最高,但是不适合在多线程中使用,容易造成数据错乱或者cpu飙升 

LinkedHashMap:与HashMap类似,不过会记录插入排序,迭代时会根据排序值展示

TreeMap:实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。

HashTable: 线程同步的散列表,put和get均加锁,效率较低

ConcurrentHashMap:线程安全的散列表,将散列表分段为16个区域,每个区域都可进行加锁,故在多线程下均比HashMap效率高 ,但在单线程下HashMap更高

 

HashMap如何实现线程安全: 使用Collections.synchronizedMap进行同步

 

总结:

Map中,HashMap具有超高的访问速度,如果我们只是在Map 中插入、删除和定位元素,而无关线程安全或者同步问题,HashMap 是最好的选择。

如果考虑线程安全或者写入速度的话,可以使用HashTable或者ConcurrentHashMap,ConcurrentHashMap相对效率最高

如果想要按照存入数据先入先出的进行读取。 那么使用LinkedHashMap

如果需要让Map按照key进行升序或者降序排序,那就用TreeMap

 

 

map性能比较:hashMap、concurrentHashMap在jmh工具下的表现

在原有的数据结构当中: 经常用到数组和链表;

 

package jmt;

 

import org.openjdk.jmh.annotations.*;

import org.openjdk.jmh.runner.Runner;

import org.openjdk.jmh.runner.options.Options;

import org.openjdk.jmh.runner.options.OptionsBuilder;

 

import java.util.Collections;

import java.util.HashMap;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.TimeUnit;

 

//@OutputTimeUnit(TimeUnit.NANOSECONDS)

//@OutputTimeUnit(TimeUnit.SECONDS)

@OutputTimeUnit(TimeUnit.MICROSECONDS)

//@BenchmarkMode(Mode.AverageTime)

@BenchmarkMode(Mode.Throughput)

public class MapTest {

 

static Map hashMap=new HashMap();

static Map synHashMap= Collections.synchronizedMap(new HashMap());

static Map conHashMap=new ConcurrentHashMap();

 

public static void main(String[] args) throws Exception {

setup();

Options options = new OptionsBuilder().include(MapTest.class.getName()).forks(1).warmupIterations(2).measurementIterations(20).build();

new Runner(options).run();

}

 

// @Setup

public static void setup(){

for(int i=0;i<10000;i++){

String t=Integer.toString(i);

hashMap.put(t,t);

synHashMap.put(t,t);

conHashMap.put(t,t);

}

}

 

@Benchmark

@Threads(5)

public void getHashMap() {

hashMap.get("4");

}

@Benchmark

@Threads(5)

public void getSynHashMap() {

synHashMap.get("4");

}

@Benchmark

@Threads(5)

public void getConHashMap() {

conHashMap.get("4");

}

 

@Benchmark

@Threads(5)

public void hashMapSi() {

hashMap.size();

}

@Benchmark

@Threads(5)

public void synHashMapSi() {

synHashMap.size();

}

@Benchmark

@Threads(5)

public void conHashMapSi() {

conHashMap.size();

}

}

 

在jdk环境下: 5个线程并发,HashMap效率最高,ConcurrentHashMap效率较低,但与HashMap处于同一个梳理级。同步的HashMap效率最低,差了100个数量级

 

 

 

散列表在投资交易系统中的使用场景

散列表在投资交易系统中的使用场景主要分为以下几种:

静态参数、牌价参数、风控用户 

 

静态参数:由于静态参数基本上在系统初始化中进行加载,后续的修改操作很少,故对线程同步的要求不高,使用HashMap进行同步即可

牌价参数:

牌价处理场景:牌价参数处于高效的写入与读取。但是由于牌价参数仅有一个线程进行写入,故目前HashMap也可满足要求 。

牌价并发清除:但后续在推送模块后,

会定时清理推送的牌价数据,故有另一条线程进行牌价删除。两条线程会进行并发处理。此时HashMap存在一定隐患,建议使用ConcurrentHashMap

进行处理 

风控区域用户:风控区域用户map分为:安全区域map、中等区域map和高危区域map;这三个map均处于多个并发线程的读写操作当中,故使用ConcurrentHashMap

进行处理

 

散列表常见错误方式与解决

HashMap多线程进行操作并发时,容易导致死循环:

在多线程环境下HashMap不仅仅在存入数据时会发生混乱,在HashMap扩容时多个线程可能会同时判定HashMap需要扩容而给他进行扩容,从而导致HashMap死循环

 

 

bat面试问题汇总

 “你用过HashMap吗?” “什么是HashMap?你为什么用到它?”

  几乎每个人都会回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null键值和值,而HashTable则不 能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等。这显示出你已经用过HashMap,而且 对它相当的熟悉。但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于HashMap的更多基础的细节。面试官可能会问出下面的问题:

  你知道HashMap的工作原理吗?” “你知道HashMap的get()方法的工作原理吗?”

  你也许会回答“我没有详查标准的Java API,你可以看看Java源代码或者Open JDK。”“我可以用Google找到答案。”

  但一些面试者可能可以给出答案,“HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put()方法传递键和值时,我们先对键调用 hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。”这里关键点在于指出,HashMap是在 bucket中储存键对象和值对象,作为Map.Entry。这一点有助于理解获取对象的逻辑。如果你没有意识到这一点,或者错误的认为仅仅只在 bucket中存储值的话,你将不会回答如何从HashMap中获取对象的逻辑。这个答案相当的正确,也显示出面试者确实知道hashing以及 HashMap的工作原理。但是这仅仅是故事的开始,当面试官加入一些Java程序员每天要碰到的实际场景的时候,错误的答案频现。下个问题可能是关于 HashMap中的碰撞探测(collision detection)以及碰撞的解决方法:

  当两个对象的hashcode相同会发生什么? 从这里开始,真正的困惑开始了,一些面试者会回答因为 hashcode相同,所以两个对象是相等的,HashMap将会抛出异常,或者不会存储它们。然后面试官可能会提醒他们有equals()和 hashCode()两个方法,并告诉他们两个对象就算hashcode相同,但是它们可能并不相等。一些面试者可能就此放弃,而另外一些还能继续挺进, 他们回答“因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生因为HashMap使用单向链表存储对象,这个 Entry(包含有键值对的Map.Entry对象)会存储在单向链表中。”这个答案非常的合理,虽然有很多种处理碰撞的方法,这种方法是最 简单的,也正是HashMap的处理方法。但故事还没有完结,面试官会继续问:

  “如果两个键的hashcode相同,你如何获取值对象?” 面试者会回答:当我们调用get()方 法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。面试官提醒他如果有两个值对象储存在同一个bucket,他给 出答案:将会遍历单向链表直到找到值对象。面试官会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?除非面试者直到 HashMap在单向链表中存储的是键值对,否则他们不可能回答出这一题。

  其中一些记得这个重要知识点的面试者会说,找到bucket位置之后,会调用keys.equals()方法去找到单向链表中正确的节点,最终找到要找的值对象。完美的答案!

  许多情况下,面试者会在这个环节中出错,因为他们混淆了hashCode()和equals()方法。因为在此之前hashCode()屡屡出现,而 equals()方法仅仅在获取值对象的时候才出现。一些优秀的开发者会指出使用不可变的、声明作final的对象,并且采用合适的equals()和 hashCode()方法的话,将会减少碰撞的发生,提高效率不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度使用 String,Interger这样的wrapper类作为键是非常好的选择

  如果你认为到这里已经完结了,那么听到下面这个问题的时候,你会大吃一惊。“如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?”除 非你真正知道HashMap的工作原理,否则你将回答不出这道题。默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时 候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放 入新的bucket数组中。这个过程叫作rehashing因为它调用hash方法找到新的bucket位置

  如果你能够回答这道问题,下面的问题来了:“你了解重新调整HashMap大小存在什么问题吗?”你可能回答不上来,这时面试官会提醒你当多线程的情况下,可能产生条件竞争(race condition)。

  当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整 大小的过程中,存储在单向链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在 单向链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。这个时候,你可以质问面试官,为什么这么奇怪,要在多线程的环境下使用HashMap 呢?:)

  热心的读者贡献了更多的关于HashMap的问题:

  1. 为什么String, Interger这样的wrapper类适合作为键? String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final 的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算 hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象 的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的 hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。

  2. 我们可以使用自定义的对象作为键吗? 这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了

  3. 我们可以使用CocurrentHashMap来代替HashTable吗?这是另外一个很热门的面试题,因为 ConcurrentHashMap越来越多人用了。我们知道HashTable是synchronized的,但是ConcurrentHashMap 同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁ConcurrentHashMap当然可以代替HashTable,但是 HashTable提供更强的线程安全性

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值