public V put(K key, V value) { //Entry<K,V>[] table,一个Entry数组 if (table == EMPTY_TABLE) { //初始化数组容量 inflateTable(threshold); } if (key == null) return putForNullKey(value); //HashMap自带的hash()方法,让hashcode更加散列,使得元素分布更为均匀 int hash = hash(key); //hash & (length-1) 等价于 hash%length,但实现思路不一样,%运算比较慢 //由于length一定为2的幂次方数,length-1低位一定全为1,当进行hash & (length-1)运算时,则保证了index实际上取的值就是与length-1同长度的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; } /** * Inflates the table. */ private void inflateTable(int toSize) { // Find a power of 2 >= toSize // 找到一个比toSize大的最小2的幂次方数。假设toSize=15,则capacity的值为16。 // 如果toSize本身是2的幂次方数,则返回toSize。 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); } void addEntry(int hash, K key, V value, int bucketIndex) { //threshold = table.length * loadFactor,即数组长度乘以加载因子。 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
Entry<K,V>[] table
数组为空时,会调用java.util.HashMap#inflateTable
方法初始化一个数组。根据HashMap实例化时传入的initialCapacity
(不传时默认为16)通过inflateTable
方法得出一个数组长度。java.util.HashMap#transfer
方法中)。头插法
赋值到新数组中,然后重新计算阈值。/** * Returns index for hash code h. */ static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); }
假设数组长度是16,则进行&运算的实际值就是15,二进制表示 0000 1111
,hashcode 是 0101 0010
。当进行&运算时,此时不论hashcode高四位如何变化,下标取值都是 0010
,所以当key不同时,数组下标是有可能相同的。
HashMap本身有一个hash方法,目的是为了让hashcode更加散列,使得元素分布更为均匀。
目的是为了分散元素,使链表中的元素分散到数组中,使得链表变短,提高HashMap的访问速度。因为数组的访问性能由于链表。
HashMap中有个remove方法,如果使用此方法进行移除元素是,有可能会抛出异常。
原因:HashMap维护了一个modCount的属性,每次对HashMap进行修改时,modCount都会自增一次。当使用循环去遍历删除时,编译的本质时一个迭代器,迭代器初始化时会有一个expectedModCount
属性,当这两个属性不相等时就会抛出异常。
这是一种fast-failed容错机制。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //转红黑树 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
&
操作,算出数组下标(假设为i),如果当前下标不存在元素,则创建一个Node对象,将Node对象赋值给第i个元素。数组长度翻倍。
不同于HashMap使用Entry数组实现,ConcurrentHashMap使用的是Segment数组实现的。Segment是由HashEntry数组实现的。
构造函数:
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) { if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; while (ssize < concurrencyLevel) { ++sshift; //与HashMap类似,ssize是大于concurrencyLevel最小的二的幂次方数 ssize <<= 1; } this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize; if (c * ssize < initialCapacity) //每个segment中hashEntry的数量。向上取整,以确保每个元素都能存放,不会丢失 ++c; //MIN_SEGMENT_TABLE_CAPACITY 默认为2 int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; // create segments and segments[0] Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }
UNSAFE.getObject
获取key所在的Segment对象。java.util.concurrent.locks.ReentrantLock#tryLock()
方法尝试获取锁,获取锁的过程中会遍历链表,如果当前的key不存在,会新建一个node插入到头部并返回。onlyIfAbsent
参数为false,则会进行更新。java.util.concurrent.ConcurrentHashMap.Segment#rehash
进行扩容)时,首先获取到当前Segment对象内部的HashEntry数组(变量名为table
),然后进行双倍扩容,然后遍历数组及HashEntry链表,根据key的hash值和新数组长度-1进行与计算,得到当前key的新的数组下标,然后将这个key对应的元素使用头插法转移到新数组中计算好的下标数组中。注意:链表中相连的元素,如果计算的index相等只需要转移第一个(对象的引用)即可,后续的链表元素引用不需要改变。使用UNSAFE.getObjectVolatile
保证获取的值为内存中最新的值。
特性:
当我们在对红黑树进行插入和删除等操作时,对树做了修改,那么可能会违背红黑树的性质。为了保持红黑树的性质,通过对树进行旋转(例如左旋和右旋操作),即修改树中某些结点的颜色及指针结构,以达到对红黑树进行插入、删除结点等操作时,红黑树依然能保持它特有的性质。
插入过程首先是根据一般二叉查找树的插入步骤, 把新结点插入到某个叶结点的位置上,然后将新结点着为红色。 为了保证红黑树的性质能继续保持,再对有关结点重点着色并旋转。
java8中java.util.HashMap.TreeNode#balanceInsertion
方法有对这一操作的实现。