HashMap

目录

1. HashMap中的成员变量:

1.1. 为什么HashMap的阈值是0.75, 为什么JDK1.8后链表切换红黑树长度为8

2. HashMap结构

2.1 其他的成员变量

3. HashMap中的成员方法

3.1 构造方法

3.2 put操作

3.2.1 put的执行流程

3.2.2 Hash算法

3.2.3 put()源码

3.2.4 resize方法()

 3.2.5 remove()

3.2.6 get()

4. JDK1.7中的rehash

5. fail-fast机制

6. JDK 1.7中HashMap线程不安全


提在前面的结论:

当table > 64 链表大于等于8 发生树化,table < 64,时,使用put添加元素,如果table>阈值且index下标位置上不等于空,进行table的2倍扩容;默认负载因子0.75

HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式。

概述

Map是key-value形式的键值对,具体实现类包括:HashMap, TreeMap, Hashtable, LinkedHashMap等

其中:

1. HashMap中的Key无序唯一,底层使用hash算法,数据结构:数组 + 链表;HashMap中的key可以为null,放到数组0位置,唯一,线程不安全

2. Hashtable, 线程安全,使用synchronize修饰,效率较低。

3. LinkedHashMap, 是HashMap的子类,对于HashMap中的元素无序,即遍历顺序和取出顺序不同;而LinkedHashMap使用链表能够保证输出顺序一致。

4. TreeMap,底层使用红黑树实现,有序,可自定义比较器

1. HashMap中的成员变量:

    //  默认初始容量16,必须是2的幂次方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

    static final int MAXIMUM_CAPACITY = 1 << 30;  // 最大容量

    static final float DEFAULT_LOAD_FACTOR = 0.75f;  // 默认负载因子

    // 当链表长度为8转换成红黑树; 至少为8,
    static final int TREEIFY_THRESHOLD = 8; 

     // 当元素个数小于6从红黑树转换成链表
    static final int UNTREEIFY_THRESHOLD = 6;

    // 树化最小数组容量,执行流程中只有数组容量 > 64 链表长度 > 8发生树化
    static final int MIN_TREEIFY_CAPACITY = 64; 

1.1. 为什么HashMap的阈值是0.75, 为什么JDK1.8后链表切换红黑树长度为8

查看源码,HashMap上面有一段注释:

* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins.  In
* usages with well-distributed user hashCodes, tree bins are
* rarely used.  Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0:    0.60653066
* 1:    0.30326533
* 2:    0.07581633
* 3:    0.01263606
* 4:    0.00157952
* 5:    0.00015795
* 6:    0.00001316
* 7:    0.00000094
* 8:    0.00000006

树的结点占用空间是常规节点的2倍,因此在存储初期使用链表的空间消耗要小于红黑树;

理想哈希分布下,结点中数据的个数呈泊松分布,每个数组结点中的数据个数应该是均匀的,并且对于每个结点达到 长度8的概率 极低

但是为了避免较差的哈希计算(比如用户自己修改了参数,打破理想情况)因此设定超过8转换红黑树,从O(n)的查询变为O(logn)

还有一种说法是根据平均查找长度:

平均查找长度:

顺序查找:

从表的一端开始,顺序扫描线性表,依次将扫描到的节点关键字和给定值k相比较。

等概率条件下...平均查找长度:ASL = (n+....+2+1)/n= (n+1)/2。

原因:

  红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

选择6和8的原因:

        避免频繁插入删除在 len = 8 徘徊,频繁切换

2. HashMap结构

// HashMap中以Node数组作为主体
transient Node<K,V>[] table; 

// JDK1.8后采用Node结点,本质也是Map.Entry
static class Node<K,V> implements Map.Entry<K,V>

// Node中的成员变量
final int hash;
final K key;
V value;
Node<K,V> next;  // 用于构建链表的next

2.1 其他的成员变量

// 记录Map中的实际元素个数
transient int size;

// 当修改map结构的时候,使用put, remove会修改这个字段
// 这个字段会出现在fast-fail机制中,用于避免并发错误
transient int modCount;

// 阈值 = capacity * load factor
int threshold;

3. HashMap中的成员方法

JDK1.8中,对于Node的初始化是在第一次put的时候完成,采用懒加载的方式

3.1 构造方法

默认无参构造:初始容量16,加载因子0.75

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

有参构造,提供容量:

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);
	this.loadFactor = loadFactor;
	// 调整HashMap容量大小,保证为2的幂次方
	this.threshold = tableSizeFor(initialCapacity);
}
// 计算距离cap最近的2的幂次方
static final int tableSizeFor(int cap) {
	// 首先将容量减1,避免给定参数是16等2的幂次方
	int n = cap - 1;
	// 通过 无符号右移,或运算调整给定参数,将末尾参数转变为全1,如00001111
	// 最后一行调整: n + 1, 变为 00010000
	n |= n >>> 1;
	n |= n >>> 2;
	n |= n >>> 4;
	n |= n >>> 8;
	n |= n >>> 16;
	return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3.2 put操作

3.2.1 put的执行流程

        对于给定的key-value,首先根据hash算法根据key和数组容量计算出数组中的索引下标。

        1. 如果索引位置为空,直接存入。

        2. 如果索引位置不为空,说明产生hash冲突,通过equals比较链表中的元素。如果不存在添加在链表尾部(JDK1.8中采用尾插法);否则,覆盖原来的元素value

        3. 当插入后如果链表长度大于8并且数组中容量大于64,进行红黑树的转换;如果数组容量小于64,那么进行2倍扩容。

3.2.2 Hash算法

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

        进行高位异或的原因:可以看到hash()上面的注释,HashMap中的table采用二进制掩码,如果只使用低位2进制发生哈希冲突的概率会很高,并且这样做会导致高位的hash完全没有用处。

        所以进行哈希值右移16,将高位也参与到运算当中;而且由于采用2进制掩码,之后计算索引的结果完全取决于计算后的h。看下面这句话:

        索引计算:由于数组容量为2幂次大小,减一后末尾均为1,进行&运算的结果完全取决于hash

i = (n - 1) & hash]

3.2.3 put()源码

核心是putVal方法

 public V put(K key, V value) {
     return putVal(hash(key), key, value, false, true);
}

 中间会用到resize()方法,主要用于调整map的大小,先给出懒加载部分代码:

// 懒加载:默认容量16,阈值=16*0.75 = 12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
// onlyIfAbsent含义:if true, don't change existing value
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
			   boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	// jdk1.8中采用懒加载的方式,在第一次put的时候进行初始化,看resize()方法
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	// 计算key在数组中的索引,创建Node结点放入链表
	if ((p = tab[i = (n - 1) & hash]) == null)  
		tab[i] = newNode(hash, key, value, null);
	// 发生hash冲突
	else {
		Node<K,V> e; K k;
		// 情况1:发生冲突,但是key在存在头结点,直接覆盖原来的value
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			e = p;
		// 情况2:如果是红黑树的结点,执行相应的插入操作
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		// 情况3:不存在,使用尾插法插入到链表中
		else {
			// 遍历链表
			for (int binCount = 0; ; ++binCount) {
				// 到达链表尾部,插入新结点
				if ((e = p.next) == null) {
					p.next = newNode(hash, key, value, null);
					// TREEIFY_THRESHOLD = 8, 当插入元素大于等于8,进行树化(可以点击去看还需要满足数组>64)
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				}
				// 访问链表的过程中发现equals,直接退出
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		// 使用拉链法解决hash冲突,将新值覆盖旧值
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	// 处理fast-fail
	++modCount;
	// 超过阈值进行扩容
	if (++size > threshold)
		resize();
	afterNodeInsertion(evict);
	return null;
}

If the map previously contained a mapping for the key, the old value is replaced.

当映射后的值相同,旧值被替换;Set中好像是抛弃

总结:put执行流程

3.2.4 resize方法()

Initializes or doubles table size.  If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move
with a power of two offset in the new table.

用来初始化或进行二倍扩容;如果为空,用初始的容量进行分配(上面说过了)。由于我们使用二倍扩容,每一个元素要么保持在原地,要么为 oldIndex + capacity

上面这段话意思:(扩容后,元素位置最多影响一位,判断这个位置是0还是1

resize()方法较长,拆成两块,总体代码是连续的。

第一阶段是处理初始化和计算新表容量的部分:

Node<K,V>[] oldTab = table;
// 获取旧map的容量和阈值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
	// 如果旧表容量已经大于最大值了,直接赋值为Integer_MAX_VALUE
	if (oldCap >= MAXIMUM_CAPACITY) {
		threshold = Integer.MAX_VALUE;
		return oldTab;
	}
	// 如果能扩容变为原来的2倍
	else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
			 oldCap >= DEFAULT_INITIAL_CAPACITY)
		newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
	newCap = oldThr;
// 懒加载走的下面代码
else {               // zero initial threshold signifies using defaults
	newCap = DEFAULT_INITIAL_CAPACITY;
	newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 看上面分支,只有用阈值作为容量的时候才会走下面代码;然后重新计算阈值
if (newThr == 0) {
	float ft = (float)newCap * loadFactor;
	newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
			  (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})

第二阶段,元素转移新表的过程:

// 开始创建新表,元素转移阶段
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
	// 遍历旧表数组,转移元素
	for (int j = 0; j < oldCap; ++j) {
		Node<K,V> e;
		if ((e = oldTab[j]) != null) {
			oldTab[j] = null;
			if (e.next == null)
				// 计算新表中的hash索引,当数组中只有一个元素的时候,直接移动
				newTab[e.hash & (newCap - 1)] = e;
			else if (e instanceof TreeNode)
				((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
			else { // preserve order,重新计算链表中元素到新数组中的索引
				Node<K,V> loHead = null, loTail = null;
				Node<K,V> hiHead = null, hiTail = null;
				Node<K,V> next;
				// JDK1.8中通过遍历链表,然后分离出两条子链(0/1)
				do {
					next = e.next;
					// 表示当前元素索引位置不变
					if ((e.hash & oldCap) == 0) {
						if (loTail == null)
							loHead = e;
						else
							loTail.next = e;
						loTail = e;
					}
					// 当前元素索引位置:原位置 + 旧数组容量
					else {
						if (hiTail == null)
							hiHead = e;
						else
							hiTail.next = e;
						hiTail = e;
					}
				} while ((e = next) != null);
				// 将新链表添加到新数组中
				if (loTail != null) {
					loTail.next = null;
					newTab[j] = loHead;
				}
				if (hiTail != null) {
					hiTail.next = null;
					newTab[j + oldCap] = hiHead;
				}
			}
		}
	}
}
return newTab;

跟着 这个 up看源码:

HashMap夺命14问,你能坚持到第几问?_wenwenaier的博客-CSDN博客

 3.2.5 remove()

final Node<K,V> removeNode(int hash, Object key, Object value,
						   boolean matchValue, boolean movable) {
	Node<K,V>[] tab; Node<K,V> p; int n, index;
	
	// 如果数组有元素,按照key获取索引位置Node:p
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(p = tab[index = (n - 1) & hash]) != null) {
		Node<K,V> node = null, e; K k; V v;
		
		// 如果数组元素等于key
		if (p.hash == hash &&
			((k = p.key) == key || (key != null && key.equals(k))))
			node = p;
		else if ((e = p.next) != null) {
			// 如果是树结点
			if (p instanceof TreeNode)
				node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
			// 普通链表结点,遍历元素
			else {
				do {
					if (e.hash == hash &&
						((k = e.key) == key ||
						 (key != null && key.equals(k)))) {
						node = e;
						break;
					}
					p = e;
				} while ((e = e.next) != null);
			}
		}
		if (node != null && (!matchValue || (v = node.value) == value ||
							 (value != null && value.equals(v)))) {
			// 删除树结点
			if (node instanceof TreeNode)
				((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
			// 删除头结点
			else if (node == p)
				tab[index] = node.next;
			// 删除链表中结点
			else
				p.next = node.next;
			++modCount;
			--size;
			afterNodeRemoval(node);
			return node;
		}
	}
	return null;
}

3.2.6 get()

final Node<K,V> getNode(int hash, Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & hash]) != null) {
		// 如果第一个元素满足条件直接返回
		if (first.hash == hash && // always check first node
			((k = first.key) == key || (key != null && key.equals(k))))
			return first;
		// 遍历链表,检查是否为红黑树结点
		if ((e = first.next) != null) {
			if (first instanceof TreeNode)
				return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	return null;
}

4. JDK1.7中的rehash

        这个值一般都为false, 为true的时候表示需要重新计算一次hash, 计算结果主要来自 容量和默认threshold的关系,可以在IDEA中通过参数 -D jdk.map.althashing.threshold = xxx设置;当容量大于等于这个数发生重新hash计算;默认是Integer.MAX_VALUE

5. fail-fast机制

        fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。fail-fast机制并不保证在不同步的修改下一定会抛出异常,它只是尽最大努力去抛出,所以这种机制一般仅用于检测bug。

HashMap中有这样一个字段:modCount; 当进行put 和 remove 的时候都会进行 ++

This field is used to make iterators on Collection-views of the HashMap fail-fast. 

这个值用来使迭代HashMap容器视图是 快速失败

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 省略
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        // 省略

            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }

实现原理:

        在迭代的时候会调用next()方法,如果当前ModCount和expectedModCount不相等,说明在遍历过程中有线程调用了remove() 或者 put()方法,修改了结构,发生并发异常,终止操作;这就是fail-fast机制

final Node<K,V> nextNode() {
       Node<K,V>[] t;
       Node<K,V> e = next;
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
       if (e == null)
           throw new NoSuchElementException();
       if ((next = (current = e).next) == null && (t = table) != null) {
           do {} while (index < t.length && (next = t[index++]) == null);
       }
        return e;
}

6. JDK 1.7中HashMap线程不安全

多线程环境下,两个线程创建HashMap,然后进行扩容操作,使用双指针进行元素转移;假设线程2被阻塞然后线程1完成元素扩容,但是线程2的结点指针是执行线程1的,在恢复后从线程1上将元素转移到自己的容器中,但是在转移过程中,使用的where(next != null) , 最终可能导致执行e.next后产生环形链表

        对于1.7和1.8而言,HashMap都是现成不安全的,但是对于1.7来说,采用头插法,while循环,当多线程环境进程上下文切换时,可能导致死锁问题。1.8中进行了解决,维护原链表顺序稳定。

        1.8中的线程不安全体现在put时候多个线程可能导致数据的覆盖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值