Java集合——HashMap、HashTable以及ConCurrentHashMap异同比较

转发:https://www.cnblogs.com/zx-bob-123/archive/2017/12/26/8118074.html

0. 前言

 

HashMap和HashTable的区别一种比较简单的回答是:

(1)HashMap是非线程安全的,HashTable是线程安全的。

(2)HashMap的键和值都允许有null存在,而HashTable则都不行。

(3)因为线程安全、哈希效率的问题,HashMap效率比HashTable的要高。

但是如果继续追问:Java中的另一个线程安全的与HashMap功能极其类似的类是什么?

同样是线程安全,它与HashTable在线程同步上有什么不同?带着这些问题,开始今天的文章。

本文为原创,相关内容会持续维护,转载请标明出处:http://blog.csdn.net/seu_calvin/article/details/52653711。

 

1.  HashMap概述

 

Java中的数据存储方式有两种结构,一种是数组,另一种就是链表,前者的特点是连续空间,寻址迅速,但是在增删元素的时候会有较大幅度的移动,所以数组的特点是查询速度快,增删较慢。

而链表由于空间不连续,寻址困难,增删元素只需修改指针,所以链表的特点是查询速度慢、增删快。

那么有没有一种数据结构来综合一下数组和链表以便发挥他们各自的优势?答案就是哈希表。哈希表的存储结构如下图所示:

 

从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点,通过功能类似于hash(key.hashCode())%len的操作,获得要添加的元素所要存放的的数组位置。

HashMap的哈希算法实际操作是通过位运算,比取模运算效率更高,同样能达到使其分布均匀的目的,后面会介绍。

键值对所存放的数据结构其实是HashMap中定义的一个Entity内部类,数组来实现的,属性有key、value和指向下一个Entity的next。

 

 

2.  HashMap初始化

 

HashMap有两种常用的构造方法:

第一种是不需要参数的构造方法:

复制代码

static final int DEFAULT_INITIAL_CAPACITY = 16; //初始数组长度为16  
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量为2的30次方  
//装载因子用来衡量HashMap满的程度  
//计算HashMap的实时装载因子的方法为:size/capacity  
static final float DEFAULT_LOAD_FACTOR = 0.75f; //装载因子  public HashMap() {    this.loadFactor = DEFAULT_LOAD_FACTOR;    
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
//默认数组长度为16   table = new Entry[DEFAULT_INITIAL_CAPACITY];  init();    
}  

复制代码

 

第二种是需要参数的构造方法:

复制代码

public HashMap(int initialCapacity, float loadFactor) {    if (initialCapacity < 0)    throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);    if (initialCapacity > MAXIMUM_CAPACITY)    initialCapacity = MAXIMUM_CAPACITY;    if (loadFactor <= 0 || Float.isNaN(loadFactor))    throw new IllegalArgumentException("Illegal load factor: " + loadFactor);    // Find a power of 2 >= initialCapacity    int capacity = 1;    while (capacity < initialCapacity)    capacity <<= 1;    this.loadFactor = loadFactor;    threshold = (int)(capacity * loadFactor);    table = new Entry[capacity];    init();    
}   

复制代码

 

从源码可以看出,初始化的数组长度为capacity,capacity的值总是2的N次方,大小比第一个参数稍大或相等。

 

3.  HashMap的put操作

复制代码

public V put(K key, V value) {    if (key == null)    return putForNullKey(value);    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;    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    V oldValue = e.value;    e.value = value;    e.recordAccess(this);    return oldValue;    }    }          
modCount++;    addEntry(hash, key, value, i);    return null;    
}  

复制代码

 

3.1  put进的key为null

 

复制代码

private V putForNullKey(V value) {    for (Entry<K,V> e = table[0]; e != null; e = e.next) {    if (e.key == null) {    V oldValue = e.value;    e.value = value;    e.recordAccess(this);    return oldValue;    }    }    modCount++;    addEntry(0, null, value, 0);    return null;    
}   void addEntry(int hash, K key, V value, int bucketIndex) {    Entry<K,V> e = table[bucketIndex];    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    if (size++ >= threshold)    resize(2 * table.length);    } 

复制代码

从源码中可以看出,HashMap是允许key为null的,会调用putForNullKey()方法:

 

putForNullKey方法会遍历以table[0]为链表头的链表,如果存在key为null的KV,那么替换其value值并返回旧值。否则调用addEntry方法,这个方法也很简单,将[null,value]放在table[0]的位置,并将新加入的键值对封装成一个Entity对象,将其next指向原table[0]处的Entity实例。

 

size表示HashMap中存放的所有键值对的数量。

threshold = capacity*loadFactor,最后几行代码表示当HashMap的size大于threshold时会执行resize操作,将HashMap扩容为原来的2倍。扩容需要重新计算每个元素在数组中的位置,indexFor()方法中的table.length参数也证明了这一点。

但是扩容是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。比如说我们有1000个元素,那么我们就该声明new HashMap(2048),因为需要考虑默认的0.75的扩容因子和数组数必须是2的N次方。若使用声明new HashMap(1024)那么put过程中会进行扩容。

 

3.2  put进的key不为null

将上述put方法中的相关代码复制一下方便查看:

复制代码

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;    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    V oldValue = e.value;    e.value = value;    e.recordAccess(this);    return oldValue;    }    
}          
modCount++;    
addEntry(hash, key, value, i);    
return null;    
}

复制代码

 

从源码可以看出,第1、2行计算将要put进的键值对的数组的位置i。第4行判断加入的key是否和以table[i]为链表头的链表中所有的键值对有重复,若重复则替换value并返回旧值,若没有重复则调用addEntry方法,上面对这个方法的逻辑已经介绍过了。

至此HashMap的put操作已经介绍完毕了。

 

4.  HashMap的get操作

复制代码

public V get(Object key) {    if (key == null)    return getForNullKey();    int hash = hash(key.hashCode());    for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {    Object k;    if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  return e.value;    }    return null;    
}    private V getForNullKey() {    for (Entry<K,V> e = table[0]; e != null; e = e.next) {    if (e.key == null)    return e.value;    }    return null;    
}  

复制代码

 

 

如果了解了前面的put操作,那么这里的get操作逻辑就很容易理解了,源码中的逻辑已经非常非常清晰了。

需要注意的只有当找不到对应value时,返回的是null。或者value本身就是null。这是可以通过containsKey()来具体判断。

 

了解了上面HashMap的put和get操作原理,可以通过下面这个小例题进行知识巩固,题目是打印在数组中出现n/2以上的元素,我们便可以使用HashMap的特性来解决。

复制代码

public class HashMapTest {    public static void main(String[] args) {    int [] a = {2,1,3,2,0,4,2,1,2,3,1,5,6,2,2,3};    Map<Integer, Integer> map = new HashMap<Integer,Integer>();    for(int i=0; i<a.length; i++){    if(map.containsKey(a[i])){    int tmp = map.get(a[i]);    tmp+=1;    map.put(a[i], tmp);    }else{    map.put(a[i], 1);    }    }    Set<Integer> set = map.keySet();          
for (Integer s : set) {    if(map.get(s)>=a.length/2){    System.out.println(s);    }    }  }    
} 

复制代码

5.  HashMap和HashTable的对比

 

HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是:

(1)HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都经过synchronized修饰。

(2)因为同步、哈希性能等原因,性能肯定是HashMap更佳,因此HashTable已被淘汰。

(3) HashMap允许有null值的存在,而在HashTable中put进的键值只要有一个null,直接抛出NullPointerException。

(4)HashMap默认初始化数组的大小为16,HashTable为11。前者扩容时乘2,使用位运算取得哈希,效率高于取模。而后者为乘2加1,都是素数和奇数,这样取模哈希结果更均匀。

这里本来我没有仔细看两者的具体哈希算法过程,打算粗略比较一下区别就过的,但是最近师姐面试美团移动开发时被问到了稍微具体一些的算法过程,我也是醉了…不过还是恭喜师姐面试成功,起薪20W,真是羡慕,希望自己一年后找工作也能顺顺利利的。

言归正传,看下两种集合的hash算法。看源码也不难理解。

复制代码

//HashMap的散列函数,这里传入参数为键值对的key  
static final int hash(Object key) {  int h;  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
}   
//返回hash值的索引,h & (length-1)操作等价于 hash % length操作, 但&操作性能更优  
static int indexFor(int h, int length) {  // length must be a non-zero power of 2  return h & (length-1);  
}  //HashTable的散列函数直接在put方法里实现了  
int hash = key.hashCode();  
int index = (hash & 0x7FFFFFFF) % tab.length;  

复制代码

 

 

 

 

6.  HashTable和ConCurrentHashMap的对比

 

先对ConcurrentHashMap进行一些介绍吧,它是线程安全的HashMap的实现。

HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。

ConcurrentHashMap算是对上述问题的优化,其构造函数如下,默认传入的是16,0.75,16。

复制代码

public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  {    //…  int i = 0;    int j = 1;    while (j < paramInt2) {    ++i;    j <<= 1;    }    this.segmentShift = (32 - i);    this.segmentMask = (j - 1);    this.segments = Segment.newArray(j);    //…  int k = paramInt1 / j;    if (k * j < paramInt1)    ++k;    int l = 1;    while (l < k)    l <<= 1;    for (int i1 = 0; i1 < this.segments.length; ++i1)    this.segments[i1] = new Segment(l, paramFloat);    }    public V put(K paramK, V paramV)  {    if (paramV == null)    throw new NullPointerException();    int i = hash(paramK.hashCode()); //这里的hash函数和HashMap中的不一样  return this.segments[(i >>> this.segmentShift & this.segmentMask)].put(paramK, i, paramV, false);    
}  

复制代码

 

ConcurrentHashMap引入了分割(Segment),上面代码中的最后一行其实就可以理解为把一个大的Map拆分成N个小的HashTable,在put方法中,会根据hash(paramK.hashCode())来决定具体存放进哪个Segment,如果查看Segment的put操作,我们会发现内部使用的同步机制是基于lock操作的,这样就可以对Map的一部分(Segment)进行上锁,这样影响的只是将要放入同一个Segment的元素的put操作,保证同步的时候,锁住的不是整个Map(HashTable就是这么做的),相对于HashTable提高了多线程环境下的性能,因此HashTable已经被淘汰了。

 

7.  HashMap和ConCurrentHashMap的对比

最后对这俩兄弟做个区别总结吧:

(1)经过4.2的分析,我们知道ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

(2)HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/423594.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不用AJAX框架实现AJAX效果

AJAX( Asynchronous JavaScript and XML),即&#xff1a;javascript和XML; 是一种进行页面局部异步处理数据的技术&#xff0c;用AJAX向服务器发送请求和获取服务器返回的数据并且更新到界面中&#xff0c;不是整个页面的刷新&#xff0c;而是HTML页面中使用JAVASCRIPT创建XMLH…

Java 面试题及答案

JAVA基础 JAVA中的几种基本类型&#xff0c;各占用多少字节&#xff1f; 下图单位是bit,非字节 1B8bit String能被继承吗&#xff1f;为什么&#xff1f; 不可以&#xff0c;因为String类有final修饰符&#xff0c;而final修饰的类是不能被继承的&#xff0c;实现细节不允许…

asp.net如何生成图片验证码

新建一个页面image.aspx,添加命名空间&#xff1a; usingSystem.Drawing.Imaging;usingSystem.IO;然后在Page_load事件拷入如下代码&#xff1a; //生成4位的验证码stringtmp RndNum(4); HttpCookie a newHttpCookie("ImageV",tmp); Response.Cookies.Add(a…

Java中如何实现代理机制(JDK、CGLIB)

代理分为两种&#xff1a; 1.静态代理 2.动态代理 动态代理又分为两种&#xff1a;jdk 实现 &#xff1b;Cglib 实现 3.Java中如何实现代理机制(JDK、CGLIB) JDK动态代理&#xff1a;代理类和目标类实现了共同的接口&#xff0c;用到InvocationHandler接口。CGLIB动态代理…

java面试题43要使某个类能被同一个包中的其他类访问,但不能被这个包以外的类访问,可以( )

java面试题43要使某个类能被同一个包中的其他类访问&#xff0c;但不能被这个包以外的类访问&#xff0c;可以( ) A让该类不使用任何关键字 B使用private关键字 C 使用protected关键字 D 使用void关键字 答案为A 我是歌谣&#xff0c;如果有什么不合理之处指出。我是歌谣&…

在VB应用程序中调用Excel2000

Visual Basic简称(VB)是设计Windows应用程序强有力的开发工具&#xff0c;“全球绝大多数Windows应用程序都是用VB开发的”&#xff1b; Excel是目前使用最广泛的办公应用软件之一&#xff0c;它具有强大的数学分析与计算功能&#xff0c;包括很多VB没有的求值数学表达式的函数…

shiro学习(7):shiro连接数据库 方式二

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 我们先看一下目录结构 首先 jar包引入 pom.xml文件 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0&quo…

shiro学习(8):shiro连接数据库 三

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 我们先看一下目录结构 首先 log4j.properties ### \u914D\u7F6E\u6839 ### log4j.rootLogger error,console ,fileAppender,dailyRollingFile,ROLLING_FILE,MAIL,DATABASE### \u8BBE\u7…

Java 中常用缓存Cache机制的实现

转&#xff1a;https://www.cnblogs.com/JAYIT/p/5647924.html 所谓缓存&#xff0c;就是将程序或系统经常要调用的对象存在内存中&#xff0c;一遍其使用时可以快速调用&#xff0c;不必再去创建新的重复的实例。这样做可以减少系统开销&#xff0c;提高系统效率。 所谓缓存&…

巧用小程序·云开发实现邮件发送功能丨实战

先看效果图&#xff1a; 通过上面的日志&#xff0c;可以看出我们是158开头的邮箱给250开头的邮箱发送邮件&#xff0c;下面是成功接收到的邮件。 准备工作 1、qq邮箱一个2、开通你的qq邮箱的授权码&#xff08;会具体讲解&#xff09;3、注册自己的小程序&#xff08;因为只有…

shiro学习(10):servelet实现权限认证一

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 在pom.xml里面添加 <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.2.3</version></dep…

JAVA多线程及线程状态转换

转发:https://www.cnblogs.com/nwnu-daizh/p/8036156.html 以下内容整理自&#xff1a;http://blog.csdn.net/wtyvhreal/article/details/44176369 线程&#xff1a;是指进程中的一个执行流程。 线程与进程的区别&#xff1a;每个进程都需要操作系统为其分配独立的内存地址空…

shiro学习(11):servelet实现权限认证二

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 在pom.xml里面添加 <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>1.2.3</version></dep…

[MOSS开发]:如何使用用户控件

如果是纯手工开发web part&#xff0c;其实还是比较困难的&#xff0c;因为这种类型的web part是以类库的形式出现&#xff0c;没有可视化的界面&#xff0c;完全由代码写出来&#xff0c;包含控件的样式&#xff0c;属性&#xff0c;事件等等。开发过自定义控件的朋友可能会感…

Spring Boot----整合SpringCloud

首先比较一下Zookeeper和Eureka的区别&#xff1f; 1、CAP&#xff1a;C&#xff1a;强一致性&#xff0c;A&#xff1a;高可用性&#xff0c;P&#xff1a;分区容错性(分布式中必须有) CAP理论的核心是&#xff1a;一个分布式系统不可能同时很好的满足一致性&#xff0c;可用性…

[原创]利用Powerdesinger同步数据库的方法说明

本文主要介绍我在工作过程中如果利用PowerDesinger同步数据库设计PDM和物理数据库保持同步。PowerDesinger以下简称PD.我们经常在数据库生成后&#xff0c;在后续的开发中发现数据设计有遗漏&#xff0c;或者是少字段&#xff0c;或者是参照完整性不一致&#xff0c;那么我们都…

shiro学习(13):springMVC结合shiro完成认证

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 在pom.xml里面添加 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3…

用小程序·云开发两天搭建mini论坛丨实战

笔者最近涉猎了小程序相关的知识&#xff0c;于是利用周末时间开发了一款类似于同事的小程序&#xff0c;深度体验了小程序云开发模式提供的云函数、数据库、存储三大能力。关于云开发&#xff0c;可参考文档&#xff1a;小程序云开发。 个人感觉云开发带来的最大好处是鉴权流程…

shiro学习(14):springMVC结合shiro完成认证

工具idea 先看看数据库 shiro_role_permission 数据 shiro_user shiro_user_role 数据 在pom.xml里面添加 <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3…

mysql聚合函数rollup和cube

转:https://blog.csdn.net/liuxiao723846/article/details/48970443 一、with rollup&#xff1a; with rollup 通常和group by 语句一起使用&#xff0c;是根据维度在分组的结果集中进行聚合操作。——对group by的分组进行汇总。 假设用户需要对N个纬度进行聚合查询操作&am…