在正常发育,HashMap,HashTable,HashSet 他们批准了经常使用的按键值地图数据结构。在这里,我主要写一些平时我们使用的这些数据结构easy忽略。

HashMap

HashMap的结构

HashMap 底层是一个Entry数组来支撑的。我认为叫Entry链表数组支撑更为合适。

结构图:

每一个entry数组里面的元素要么为null要么就是一个entry链表;而每一个entry对象就是一个entry链表的节点也是一个键值对的抽象表示;

HashMap的性能因素

HashMap主要影响其性能的有两个因素,一个是初始容量。一个是加载因子;HashMap(int initialCapacity初始容量, float loadFactor加载因子),我们在遍历HashMap的时候,会对整个数组都进行遍历。也就是说性能跟entry数组的长度有关(容量)。假设将初始容量设置的过大。实际上我们没装几个东西在里面,那么遍历的时候。会遍历全部数组组元素。这里已经指出了,我们不希望容量设置的过大。那么当put数据的时候检測到容量超过我们的阀值threshold。就会又一次构造一个两倍的数组出来,从而达到扩容的母的。 
if (size++ >= threshold)  resize(2 * table.length); threshold = 当前容量*loadFactor加载因子。

我们始终要抓住一点,HashMap要常常遍历。我们应该让他在合适的时间选择扩容,避免过早的遍历更大的容量数组。所以我们应当尽量避免将loadFactor设置的过小。

哈希冲突

当我们put两个元素的时候,假设他们的哈希值都一样,或者说哈希值不一样。可是数组下标一样的时候,那么究竟谁该放在同个槽里呢?这就是通俗的哈希冲突。为了解决哈希冲突。jdk採用链表的方式来解决哈希值的冲突。

以下我们看看源代码来分析。

  1. public V put(K key, V value) {
  2. if (key == null)
  3. return putForNullKey(value);
  4. int hash = hash(key.hashCode());//计算键的哈希值
  5. int i = indexFor(hash, table.length);//找到该哈希值相应的entry数组下标
  6.  
  7. for (Entry<K,V> e = table[i]; e != null; e = e.next) {//假设entry数组下标相应的entry链表里面,put之前就存在与这个指定的key关联的entry对象。那么直接替换旧的value。并返回这个旧的value给调用者。
  8. Object k;
  9. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//当哈希算法计算出来的哈希值同样,而且(key是同一个对象||两个key equals推断同样)即表示存在旧的key关联的entry
  10. V oldValue = e.value;
  11. e.value = value;
  12. e.recordAccess(this);//这个 hashMap 无需关心。
  13. return oldValue;
  14. }
  15. }
  16.  
  17. modCount++;
  18. addEntry(hash, key, value, i);//当entry数组下标相应的entry链表没有与指定的key关联的entry对象时。添加一个新的entry对象,哈希冲突也是在这个函数里解决的。
  19.  
  20. return null;
  21. }
  22. void addEntry(int hash, K key, V value, int bucketIndex) {
  23. Entry<K,V> e = table[bucketIndex];//把旧的链表地址临时保存在一个变量中
  24. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);//採用头插法。直接插到链表的头部
  25. if (size++ >= threshold)
  26. resize(2 * table.length);
  27. }

如果我们要put两组数据,各自是put(0,0),put(10,10) ,如果计算哈希值的算法 int hash = key % 10; 那么0 和 10 的哈希值都为0,然后int i = indexFor(hash, table.length);

两组数据key相应的数组下标都是0。

那么是怎么插入的呢?先put(0,0)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGl1c2hlbmdiYW9ibG9n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

在put(10,10) 哈希冲突,在链表头部插入解决。

并发情况下HashMap的死循环问题

事实上HashMap本不该在并发环境下使用,应该考虑选择HashTable,ConcurrentHashMap。我们就来分析下HashMap的死循环问题。

当多个线程同一时候put数据的时候就有可能出现死循环的问题。

  1. void addEntry(int hash, K key, V value, int bucketIndex) {
  2. Entry<K,V> e = table[bucketIndex];//把旧的链表地址临时保存在一个变量中
  3. table[bucketIndex] = new Entry<K,V>(hash, key, value, e);//採用头插法。直接插到链表的头部
  4. if (size++ >= threshold)
  5. resize(2 * table.length);//多线程下。多个线程可能会同一时候运行这个函数
  6. }
  7. void resize(int newCapacity) {
  8.     Entry[] oldTable = table;
  9.     int oldCapacity = oldTable.length;
  10.     if (oldCapacity == MAXIMUM_CAPACITY) {
  11.         threshold = Integer.MAX_VALUE;
  12.         return;
  13.     }
  14.  
  15.     Entry[] newTable = new Entry[newCapacity];
  16.     transfer(newTable);//当2个线程同一时候运行这个转移数据到新的数组时就有可能出现故障。
  17.     table = newTable;
  18.     threshold = (int)(newCapacity * loadFactor);
  19. }
  20. void transfer(Entry[] newTable) {
  21.     Entry[] src = table;
  22.     int newCapacity = newTable.length;
  23.     for (int j = 0; j < src.length; j++) {
  24.         Entry<K,V> e = src[j];
  25.         if (e != null) {
  26.             src[j] = null;
  27.             do {//这个do while 循环要做的操作就是翻转旧的链表插入到新数组里面。
  28.                 Entry<K,V> next = e.next;//标记1。如果线程1运行完这步
  29.                 int i = indexFor(e.hash, newCapacity);
  30.                 e.next = newTable[i];
  31.                 newTable[i] = e;
  32.                 e = next;
  33.             } while (e != null);
  34.         }
  35.     }
  36. }

我们来个正常版的单线程环境下的resize操作,看图:

trasnfer(newTable)之前

trasnfer(newTable)完毕后

我们能够看出来。实际就是翻转链表插入到新容量的entry数组里面。

再来看看死循环版本号,有两个线程put数据都进行transfer(newTable) 操作,那么就会可能出现死循环。

当线程1 数据传输时,运行完了标记1的时候切换到了线程2,线程2运行了一次完整的翻转链表到新的entry数组时,线程1继续跑就会出现。

  1. void transfer(Entry[] newTable) {
  2. Entry[] src = table;
  3. int newCapacity = newTable.length;
  4. for (int j = 0; j < src.length; j++) {
  5. Entry<K,V> e = src[j];
  6. if (e != null) {
  7. src[j] = null;
  8. do {//这个do while 循环要做的操作就是翻转旧的链表插入到数组里面。
  9. Entry<K,V> next = e.next;//标记1,如果线程1运行完这步
  10. int i = indexFor(e.hash, newCapacity);
  11. e.next = newTable[i];
  12. newTable[i] = e;
  13. e = next;
  14. } while (e != null);
  15. }
  16. }
  17. }

用图分析吧:

原数组:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGl1c2hlbmdiYW9ibG9n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

此时线程1运行到了标记1,然后切换到了线程2运行一个链表的翻转插入。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGl1c2hlbmdiYW9ibG9n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

然后此时链表情况是这种。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGl1c2hlbmdiYW9ibG9n/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

线程1在do while 里面就不断的循环 do (.....)while (e.next != null),这样线程1就根本停不下来了。

HashSet

HashSet底层的存储结构是一个HashMap。HashSet 加入的每个值实际上就是在底层的HashMap里加入一个实体Entry<Key,Object> e,而这里的Object 事实上就是一个摆设。HashSet利用了HashMap的Key的唯一性,确保了在该数据结构中不会加入反复的值(保证了值的唯一性)。另外HashSet是同意插入null值的。依据HashSet的值的唯一性和高速加入的特性,我们能够想到,假设我们要高速加入大量的不能反复的元素到一个数据结构中,那么HashSet 是一个很好的选择。

HashTable

HashTable 跟HashMap 一样是一个存储键值对映射的数据结构,跟HashMap 不一样的是,HashTable 是线程安全的,HashTable 要插入的key 和 value 都不能为null。为啥不能为null呢?jdk文档里面说了。HashTable 是继承了字典的一种数据结构。我们能够在这样的字典里提高一个键值对以供查找。可是key 或者 value 不论什么一个都不能为null。

我是怎么觉得的呢?就像我们查字典一样。你总不能造个没有含义。没有表现的文字在字典里面吧,假设有我查到了,这是个null,没有含义,这全然违背了我们想通过查字典获取真相的初衷啊。

HashTable 的线程安全型是靠对每一个操作加锁的方式完毕的。

也就是锁住当前的HashTable实例对象。假设在并发大量的情况下。那么锁竞争会非常严重。我以为假设在并发情况不大的情况下当我们又想保证数据的并发安全性,我认为HashTable也是一种非常好的选择。

当然在并发量大的情况下。就优先选择ConcurrentHashMap 。

我自己写了个測试程序。在计数为2亿次的并发put測试中。不同线程数量。对HashTable  和 ConcurrentHashMap  的表现分析。

代码:

  1. package hash_set_map_table;
  2.  
  3. import java.util.Hashtable;
  4. import java.util.Random;
  5. import java.util.concurrent.ConcurrentHashMap;
  6. import java.util.concurrent.atomic.AtomicInteger;
  7.  
  8. public class HashThreadTask implements Runnable {
  9.  
  10. static int TIME = 200000000;
  11. //2个线程。并发put 200000000次,ConcurrentHashMap 31718 ms。Hashtable 35535 ms;
  12. //20个线程,并发put 200000000次。ConcurrentHashMap 38732 ms,Hashtable 48357 ms;
  13. //100个线程 并发put 200000000次,ConcurrentHashMap 36380 ms,Hashtable 46299 ms;
  14. //200个线程 并发put 200000000次。ConcurrentHashMap 35801 ms, Hashtable 50579 ms;
  15. private int threadId;
  16.  
  17. public HashThreadTask(int threadId) {
  18. this.threadId = threadId;
  19. }
  20.  
  21. public int getThreadId() {
  22. return threadId;
  23. }
  24.  
  25. public void setThreadId(int threadId) {
  26. this.threadId = threadId;
  27. }
  28.  
  29. static AtomicInteger count = new AtomicInteger();
  30.  
  31. public static Hashtable<Integer, Integer> getHashTableInstance() {
  32. return TableHolder.table;
  33. }
  34.  
  35. public static ConcurrentHashMap<Integer, Integer> getConcurrentHashMap() {
  36. return ConcurrentHashMapHolder.map;
  37. }
  38.  
  39. public static class TableHolder {
  40. public static Hashtable<Integer, Integer> table = new Hashtable<Integer, Integer>();
  41. }
  42.  
  43. public static class ConcurrentHashMapHolder {
  44. public static ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<Integer, Integer>();
  45. }
  46.  
  47. public static void main(String[] args) {
  48. for (int i = 0; i < 200; i++) {//这里控制线程数量 測试数据依次为 2个线程。20个线程。100个线程。200个线程
  49. HashThreadTask task = new HashThreadTask(i);
  50. Thread thread = new Thread(task);
  51. thread.start();
  52. }
  53.  
  54. long s = System.currentTimeMillis();
  55. while (count.get() != TIME) {
  56. }
  57. System.out.println("cost time : "+ (System.currentTimeMillis() - s) + " count:" + count.get());
  58.  
  59. }
  60.  
  61. public void run() {
  62. // ConcurrentHashMap<Integer, Integer> container = getConcurrentHashMap();
  63. Random random = new Random(System.currentTimeMillis());
  64. Hashtable<Integer, Integer> container = getHashTableInstance();
  65. do {
  66. int old = count.get();
  67. if (old < TIME) {
  68. int i = random.nextInt(10000);
  69. container.put(i, i);
  70. count.compareAndSet(old, old+1);
  71. }
  72. }
  73. while (count.get() < TIME);
  74.  
  75. }
  76.  
  77. }

结果:

//2个线程,并发put 200000000次,ConcurrentHashMap 31718 ms,Hashtable 35535 ms;

//20个线程,并发put 200000000次,ConcurrentHashMap 38732 ms,Hashtable 48357 ms;

//100个线程  并发put 200000000次。ConcurrentHashMap 36380 ms,Hashtable 46299 ms;

//200个线程  并发put 200000000次,ConcurrentHashMap 35801 ms, Hashtable 50579 ms;

在并发量不大的时候,当我们又想保证数据的并发安全性的话,我认为HashTable 优于 ConcurrentHashMap,由于Hashtable 没那么吃内存。

当在并发量大的时候,Hashtable 就输的一塌糊涂了。所以在这样的大并发环境下,我们我们不应该毫不犹豫的选择ConcurrentHashMap。

谈论HashMap,HashSet,HashTableeasy被我们忽视的更多相关文章

  1. Java 集合 HashMap & HashSet 拾遗

    Java 集合 HashMap & HashSet 拾遗 @author ixenos 摘要:HashMap内部结构分析 Java HashMap采用的是冲突链表方式 从上图容易看出,如果选择 ...

  2. HashTable HashMap HashSet区别(java)

    Hashtable: 1. key和value都不许有null值 2. 使用enumeration遍历 3. 同步的,每次只有一个线程能够访问 4. 在java中Hashtable是H大写,t小写,而 ...

  3. java HashMap HashSet的存储方式

    今天遇到一个bug,简单的说就是把自定义对象作为key 存到HashMap中之后,经过一系列操作(没有remove操作)之后 用该对象到map中取,返回null. 然后查看了HashMap的源代码,g ...

  4. HashMap,Hashset,ArrayList以及LinkedList集合的区别,以及各自的用法

    基础内容 容器就是一种装其他各种对象的器皿.java.util包 容器:Set, List, Map ,数组.只有这四种容器. Collection(集合) 一个一个往里装,Map 一对一对往里装. ...

  5. ArrayList,Vector,HashMap,HashSet,HashTable之间的区别与联系

    在编写java程序中,我们最常用的除了八种基本数据类型,String对象外还有一个集合类,在我们的的程序中到处充斥着集合类的身影!java中集合大家族的成员实在是太丰富了,有常用的ArrayList. ...

  6. HashMap/HashSet,hashCode,哈希表

    hash code.equals和“==”三者的关系 1) 对象相等则hashCode一定相等: 2) hashCode相等对象未必相等. == 是比较地址是否相等,JAVA中声明变量都是引用嘛,不同 ...

  7. java 遍历方法 及 数组,ArrayList,HashMap,HashSet的遍历

    一,遍历方法的实现原理 1.传统的for循环遍历,基于计数器的: 遍历者自己在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后,停止.主要就是需要按元素的位置来读取元素. ...

  8. java - day011 - 集合, ArrayList HashMap,HashSet, Iterator 接口, for-each 循环格式

    集合 ArrayList 丑数: 能被3,5,7整除多次, ArrayList     list 接口             | - ArrayList             | - Linked ...

  9. LinkedList,ArrayList,Vector,HashMap,HashSet,HashTable之间的区别与联系

    在编写java程序中,我们最常用的除了八种基本数据类型,String对象外还有一个集合类,在我们的的程序中到处充斥着集合类的身影!java中集合大家族的成员实在是太丰富了,有常用的ArrayList. ...

随机推荐

  1. Android KitKat 4.4 Wifi移植AP模式和网络共享的调试日志

    Tethering技术在移动平台上已经运用的越来越广泛了.它能够把移动设备当做一个接入点,其它的设备能够通过Wi-Fi.USB或是Bluetooth等方式连接到此移动设备.在Android中能够将Wi ...

  2. (转)FFMPEG解码流程

    http://www.douban.com/note/228831821/ FFMPEG解码流程:     1. 注册所有容器格式和CODEC: av_register_all()     2. 打开 ...

  3. Android应用-包装脚本批量方法

    1. 设定ant周边环境 加入用户变量: 变量名:ANDROID_SDK_ROOT 变量值:D:\Android Develop\adt-bundle-windows-x86_64-20140321\ ...

  4. oracle存储过程的例子

    oracle存储过程的例子 分类: 数据(仓)库及处理 2010-05-03 17:15 1055人阅读 评论(2)收藏 举报 认识存储过程和函数 存储过程和函数也是一种PL/SQL块,是存入数据库的 ...

  5. OpenCV面、人眼检测

    /* 功能:实现对眼睛.脸部的跟踪. 版本号:1.0 时间:2014-4-27 */ #include <opencv2/objdetect/objdetect.hpp> #include ...

  6. JXL组件生成报告错误(两)

    JXL组件生成报告 1.详细报错例如以下: usage: java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -non ...

  7. ISAPI_Rewrite不起作用的N种原因

    现在经常用到ISAPI_Rewrite,遇到的问题就是在本地测试的时候,一切没有问题,到服务器上,竟然不起作用.郁闷~经过我的一些探索,发现了比起作用的原因如下:1.IIS_WPG对ISAPI_Rew ...

  8. 【iOS开发-21】UINavigationController导航控制器初始化,导航控制器栈的push和pop跳转理解

    (1)导航控制器初始化的时候一般都有一个根视图控制器,导航控制器相当于一个栈,里面装的是视图控制器,最先进去的在最以下,最后进去的在最上面.在最上面的那个视图控制器的视图就是这个导航控制器对外展示的界 ...

  9. Play Modules Morphia 1.2.9a 之 Aggregation and Group aggregation

    聚合 和 分组聚合: PlayMorphia 它提供了基于开发人员models的友好接口 设想你定义了一个model.class Sales: @Entity public class Sales e ...

  10. RH133读书笔记(7)-Lab 7 Advanced Filesystem Mangement

    Lab 7 Advanced Filesystem Mangement Goal: Develop skills and knowlege related to Software RAID, LVM, ...