HashMap vs HashTablehtml
HashTable若是插入key/value为null的值时,会报错,可是hashmap不会,在hashmap中,null是做为第0个元素的,至关因而作了特殊化处理。算法
前者是非线程安全的,后者是线程安全的. 后者线程安全的缘由就是由于后者的每个方法上都有一个synchronized,这样虽然保障了线程安全,可是每次都要锁整个class对象,而且还会阻止其余synchronize方法的访问,因此效率低下! HashTable已经废弃了,尽可能别使用了.shell
因此综上所述,HashTable效率低的缘由是由于全部访问它的线程都必须竞争同一把锁,若是容器里面不一样的数据有多把锁,那么执行的效率就高了,因此ConcurrentHashMap使用的就是锁分段的技术.数据库
JDK1.8以后,HashMap和以前1.7不一样的是,再也不单纯的由数组+链表的方式实现的(所谓的链地址法). 1.7因为在Hash冲突的时候,在桶上造成的链表会愈来愈长,这样在查询的时候效率就会变低,1.8以后改为了由红黑树实现.即:数组
因此,JAVA8中的HashMap是由: 数组+链表/红黑树组成安全
Java7使用Entry来表示每一个HashMap中的数据节点,8中使用了Node,基本没什么区别,都是key,value,hash和next这四个属性来修饰链表,而红黑树的状况须要使用TreeNode函数
默认容量为: 16性能
这里的默认容量为何是2的n次幂?.net
查看它的put方法可知,key在Node[]中的下标为: (n - 1) & hash。若是这个n是2的N次幂,那么hash至关于和111***111作与运算,数据分散的就比较均匀。若是n不是在的N次幂,即hash有可能和1110作与运算,那么最后一位怎么与都是0,那至关于结尾是1的那几个下标永远都不会放数据了,好比0001,0011…这确定会增长碰撞的概率.线程
若是size > capacity*loadFactor的话,hashmap还会进行resize操做,会至关耗性能!因此若是事先能够肯定你要放进hashMap中的数据大小。那么应该尽可能设置成loadFactor * 2^n > initCapacity ,这样既考虑了&的问题,也避免了resize的问题.
可是后面提到会有tableSizeFor()和在put的时候考虑loadFact来保证上面这两个要求的.因此在初始化的时候,设置成本身知道的大小便可,冲突这些由hashmap自身来帮你减免!
不对,仍是得本身算下,若是你输入的initcapacity为7,那么算出来的最近的2^n为8,若是选择的是默认的0.75,即最多放入6个元素就要扩容,你放到第7个的时候,仍是要resize()…
最大的capacity为 2^30
默认负载因子为0,75,即size > capacity * 0.75,hashmap就要进行扩容
特殊状况下:
1)内存空间不少,时间效率要求很高,能够下降loadFactor(尽可能让table扩宽)
2)若是内存紧张,时间效率要求不高,能够增长loadFactor
一个桶中,bin(箱子)的存储方式由链表转成红黑树的阈值为8
一个桶中,由红黑树转成链表的阈值,resize的时候可能会用到这个值.
当桶中的bin被树化时最小的hash表容量.若是树化时bin的数量太多会进行resize扩容.注释中说MIN_TREEIFY_CAPACITY至少是 4 * TREEIFY_THRESHOLD
上面说了这么多的bin,这里该介绍下bin究竟是个什么结构了.
每一个bin在HashMap表明存储了一个K/V键值对,结构定义以下:
Hashmap中计算key的hash值
能够看到它并无直接使用Object中生成hashcode的方法,这个方法叫扰动函数,和以前的要将capacity设计成2^n同样,也是为了减小碰撞用的.
根据前面可知,key在Node[]中的下标为 key.hashCode & (n-1),咱们知道key.hashCode是一个很长的int类型的数字(范围大概40亿),而n-1显然没有这么长,若是直相与,那么只有key.hashCode的后面几位参与运算了,显然会使得碰撞很激烈!加了这个函数以后,让高位也想办法参与到运算中来,这样就有可能进一步下降碰撞的可能性了!
用于存储Node(K/V)对的hash表(数组),为何是transient?
为了解答这个问题,咱们须要明确下面事实:
Object.hashCode方法对于一个类的两个实例返回的是不一样的哈希值
能够试想下面的场景:
咱们在机器A上算出对象A的哈希值与索引,而后把它插入到HashMap中,而后把该HashMap序列化后,在机器B上从新算对象的哈希值与索引,这与机器A上算出的是不同的,因此咱们在机器B上get对象A时,会获得错误的结果。
因此说,当序列化一个HashMap对象时,保存Entry的table是不须要序列化进来的,由于它在另外一台机器上是错误的,因此属性这里为transient。
由于这个缘由,HashMap重写了writeObject与readObject 方法
保存K/V对的Set
目前hashMap中K/V对的数量
每次对这个hashmap作操做,这个modCount就会改变.(CAS?!)
Threadhold表示当容量达到该值时,会进行resize
loadFactor表示用户设置的负载因子大小
构造函数:
有三种,一种是无参的,这个没啥好看的,一种是只配置了initialCapacity,最后一种是设置了initcapacity和loadFactor。看第三种就够了,第二种不过是将loadFactor这个形参用默认0.75传入而已.
方法内部是将loadFactor这个属性设置为用户输入的大小,有意思的是tableSizeFor(initCap)这个函数,也就是说你输入一个10,hash表的大小不必定就是10. 这个函数的功能就是用来保证容量应该大于cap,且为2的整数幂.
随便写个数带进去算一下就可知道,该算法的做用让最高位的1后面的位全变为1.而后再+1,获得的就是恰巧的2的n次幂.
注意table的初始化是在第一次put的时候作的,那个时候还有考虑loadFactor再作一次tableSize的计算,那个时候获得的就是最合适的那个2的n次幂的那个数了!厉害啊!
因此,看下put流程吧!这个很重要:
方法流程以下:
方法定义以下:
False表示,会改变existing value,至关因而key相同的话会作替换.
True表示,table不在creation mode.(这是啥意思?!)
第一次或者扩容的时候会调用resize():
那么newCap=16, threshold = 0.75 * 16
Hash表大小为: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]
注意,此时的threadhold(=initcapacity接近的2^n那个值) 和 loadFactor已经有值了,可是table==null,由于没有初始化嘛,因此此时oldCap=0
此时 newCap = threadhold
threshold =newThreadHold = newCap * loadFactor (threadhold还变小了!…)
Hash表大小为: table = Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]
此时 oldTab !=null oldCap = oldTable.length oldThreadHold = threadhold
那么 newCap = oldCap << 1 为原来的一倍 newThreadholder也为oldThr的一倍,即都扩大为原先的一倍!
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
还要将原先旧Table里的数据转移到新的table中.
即 ++size > threshold 那么须要进行resize
这里让我想到有问题的可能就是这个了,首次初始化,cap为16,输入的initcapacity为15的时候,会涉及到一次扩容(若是没有冲突的话—因此这里的扩容一倍啥的操做应该是取了个折中!否则为了避免冲突再扩容一倍很耗费空间啊!并且再扩容一倍也不能保证不冲突)
resize()流程分析:
(和Java7不一样,8中resize后元素顺序是不变的)
归纳起来就是:
若是已是MAXIMUM_CAPACITY,那么返回原先数组,不然将容量扩大为原来的一倍,即newCap = oldCap << 1, newThr=oldThr << 1
newCap = oldThr, newThr随后会被指定为newCap*loadFactor
按照默认的值初始化,即initCapacity=16, newThr=0.75*16
迁移数据的流程以下:
举例:
好比原来容量是16,那么就至关于index=e.hash & 0x1111
如今容量扩大了一倍,就是32,那么index=e.hash & 0x11111
如今(e.hash & oldCap) == 0就代表:
已知: e.hash & 0x1111 = index
而且: e.hash & 0x10000 = 0
那么: e.hash & 0x11111 不也就是原先index的值!
get()流程分析:
若是获取到的Node为null则返回null,不然返回Node中存储的值。
点到getNode()中继续查看
分四步进行:
首先,若是table未空,直接返回null。不然就查找节点
再者,计算hash获得的位置刚符合,那直接返回。(只有一个bin的状况)
若是是红黑树,按红黑树的方式查找
若是是链表,逐个查找。 找不到返回null。
entrySet分析:
遍历的话,使用此种方式。比每次从Map中从新获取一个key要快多了!
不是每次都是new EntrySet(),可是暂时没找到这个东西在哪里填充的。
网上的说法是遍历的原理就是hashmap实现的原理。entrySet()该方法返回的是map包含的映射集合视图,视图的概念至关于数据库中视图。提供一个窗口,没有具体到相关数据,而真正获取数据仍是从table[]中来。(ps:能够借鉴下hashmap的foreach方法,即先遍历完数组中的第一个链表,再遍历数组中的下一个链表…)
HashMap为何线程不安全:
Java8之前线程不安全是在于在resize()的时候会在get的时候产生死循环,而之因此产生死循环是由于resize以后,元素的前后顺序会相反。
即转移的时候是这样的:每次取出旧数组的头结点的next,以后从新计算头结点在新的Hash中的位置,而后将头节点的next指向新的table[i],而后把table[i]设置成当前的头结点,那么就完成了头结点的转移。
这时候,线程一种3.next是7,线程二中7.next是3,e.next = newTable[i]就会造成了环形链表,因此在get的时候就会一直循环在这里。
而且在迭代的过程当中,若是有线程修改了map,会抛出ConcurrentModificationException错误,就是所谓的fail-fast策略。
Java8以后,由于顺序是相同的,因此上面的那个环形链表问题就没有了。可是后面那个问题仍是有的,因此仍是线程不安全的。另外还有++size的操做也不是线程安全的!
参考:
http://www.iteye.com/topic/539465 (initailCapacity为何要设置成2的n次幂?)
https://blog.csdn.net/dog250/article/details/46665743#comments (红黑树的一种解释)