标签:唤醒 重复执行 有一个 导致 bat head count 动态 als
1. 什么是Hash表
hash函数就是根据key计算出应该存储地址的位置,而哈希表是基于哈希函数建立的一种查找表
2. hash函数设计的考虑因素
3.哈希冲突的解决方案
不管hash函数设计的如何巧妙,总会有特殊的key导致hash冲突,特别是对动态查找表来说。hash函数解决冲突的方法有以下几个常用的方法
A.开放定制法(线性探索)
B.链地址法(HashMap)
C.公共溢出区法建立一个特殊存储空间,专门存放冲突的数据。此种方法适用于数据和冲突较少的情况。
D.再散列法(布隆过滤器)准备若干个hash函数,如果使用第一个hash函数发生了冲突,就使用第二个hash函数,第二个也冲突,使用第三个……
当一个关键字和另一个关键字发生冲突时,使用某种探测技术在Hash表中形成一个探测序列,然后沿着这个探测序列依次查找下去,当碰到一个空的单元时,则插入其中。基本公式为:hash(key) = (hash(key)+di)mod TableSize。其中di为增量序列,TableSize为表长。根据di的不同我们又可以分为线性探测,平方(二次)探测,双散列探测。
1)线性探测
以增量序列 1,2,……,(TableSize -1)循环试探下一个存储地址,即di = i。如果table[index+di]为空则进行插入,反之试探下一个增量。但是线性探测也有弊端,就是会造成元素聚集现象,降低查找效率。具体例子如下图:
特别对于开放定址法的删除操作,不能简单的进行物理删除,因为对于同义词来说,这个地址可能在其查找路径上,若物理删除的话,会中断查找路径,故只能设置删除标志。
//插入函数,利用线性探测法 bool Insert_Linear_Probing(int num){ //哈希表已经被装满,则不在填入 if(this->size == this->length){ return false; } int index = this->hash(num); if(this->data[index] == MAX){ this->data[index] = num; }else{ int i = 1; //寻找合适位置 while(this->data[(index+i)%this->length] != MAX){ i++; } index = (index+i)%this->length; this->data[index] = num; } if(this->delete_flag[index] == 1){//之前设置为删除 this->delete_flag[index] = 0; } this->size++; return true; }
HashMap即是采用了链地址法,也就是数组+链表的方式,HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。
//HashMap的主干数组,可以看到就是一个Entry数组,初始值为空数组{},主干数组的长度一定是2的次幂,至于为什么这么做,后面会有详细分析。 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry是HashMap中的一个静态内部类。代码如下
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构 int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
所以,HashMap的整体结构如下
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现,ConcurrentHashMap在并发编程的场景中使用频率非常之高,下面我们来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析 。
众所周知,哈希表是种非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。
HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(多线程扩容时可能造成),导致get操作时,cpu空转, 所以,在并发环境中使用HashMap是非常危险的。
HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据比喻[11],这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。java1.7后的CHM中把每个数组叫Segment,每个segment下面存的是默认16段的Hashhenery,Hashhenery解决充突是在Hashhenery下面挂载链表,我们就画图说明下分段锁
ConcurrentHashMap
初始化时,计算出Segment
数组的大小ssize
和每个Segment
中HashEntry
数组的大小cap
,并初始化Segment
数组的第一个元素;其中ssize
大小为2的幂次方,默认为16,cap
大小也是2的幂次方,最小值为2,最终结果根据初始化容量initialCapacity
进行计算,计算过程如下
if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1;
因为Segment
继承了ReentrantLock,所有segment是线程安全的,但是在1.8中放弃了Segment
分段锁的设计,使用的是Node
+CAS
+Synchronized
来保证线程安全性,而且这样设计的好处是层级降低了,锁的粒度更小了,可以说是一种优化,比喻锁的是2,那么他锁的就只是发生冲突的2下面的链表,而不像1.7样,是锁整个HashEntry;而且1.8中对链表的长度进行了优化,在1.7的链表中链表查询的复杂度是O(n),但是在1.8中为了解决这问题引入了红黑树,在1.8中当我们链表长度大于8时并且数组长度大于64时,就会发生一个链表的转换,会把单向链表转换成红黑树。
在1.7 中当执行put
方法插入数据的时候,根据key的hash值,在Segment
数组中找到对应的位置如果当前位置没有值,则通过CAS进行赋值,接着执行Segment
的put
方法通过加锁机制插入数据;假如有线程AB同时执行相同Segment
的put
方法
线程A 执行tryLock方法成功获取锁,然后把HashEntry对象插入到相应位置 线程B 尝试获取锁失败,则执行scanAndLockForPut()方法,通过重复执行tryLock()方法尝试获取锁 在多处理器环境重复64次,单处理器环境重复1次,当执行tryLock()方法的次数超过上限时,则执行lock()方法挂起线程B 当线程A执行完插入操作时,会通过unlock方法施放锁,接着唤醒线程B继续执行
但在1.8 中执行put
方法插入数据的时候,根据key的hash值在Node
数组中找到相应的位置如果当前位置的 Node
还没有初始化,则通过CAS插入数据
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //如果当前位置的`Node`还没有初始化,则通过CAS插入数据 if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin }
如果当前位置的Node已经有值,则对该节点加synchronized
锁,然后从该节点开始遍历,直到插入新的节点或者更新新的节点
if (fh >= 0) { binCount = 1; for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } }
如果当前节点是TreeBin
类型,说明该节点下的链表已经进化成红黑树结构,则通过putTreeVal
方法向红黑树中插入新的节点
else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } }
如果binCount
不为0,说明put
操作对数据产生了影响,如果当前链表的节点个数达到了8个,则通过treeifyBin
方法将链表转化为红黑树
标签:唤醒 重复执行 有一个 导致 bat head count 动态 als
原文地址:https://www.cnblogs.com/xing1/p/13775782.html