一、HashMap
HashMap 是一个散列表(哈希表),它存储的内容是键值对(key-value)映射
散列表(哈希表)是一种空间换时间的存储结构,是在算法中提升效率的一种比较常用的方式
散列表,是指可以通过关键字key直接访问到内容value的一种数据结构;其中是通过key映射到一个位置上,来直接访问value
注:
一个value对应多个key;但是一个key只能对应一个value
二、HashMap数据结构
1、HashMap源码实现
HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口
HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步
HashMap 是无序的,即不会记录插入的顺序
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
IDEA左侧导航栏 Structure (Alt + 7)查看该类方法以及成员变量
2、常量及构造方法
//这两个是限定值 当节点数大于8时会转为红黑树存储
static final int TREEIFY_THRESHOLD = 8;
//当节点数小于6时会转为单向链表存储
static final int UNTREEIFY_THRESHOLD = 6;
//红黑树最小长度为 64
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap容量初始大小;3、当前数组容量,默认16,始终保持2^n,可扩容,扩容后数组大小为当前2倍
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap容量极限
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子默认大小
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//Node是Map.Entry接口的实现类
//在此存储数据的Node数组容量是2次幂
//每一个Node本质都是一个单向链表
transient Node<K,V>[] table;
//HashMap大小,它代表HashMap保存的键值对的多少
transient int size;
//HashMap被改变的次数
transient int modCount;
//扩容阈值;下一次HashMap扩容的大小,等于 capacity * loadFactor
int threshold;
//存储负载因子的常量
final float loadFactor;
//默认的构造函数:造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//指定容量大小:构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//指定容量大小和负载因子大小:构造一个带指定初始容量和加载因子的空 HashMap
public HashMap(int initialCapacity, float loadFactor) {
//指定的容量大小不可以小于0,否则将抛出IllegalArgumentException异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//判定指定的容量大小是否大于HashMap的容量极限
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//指定的负载因子不可以小于0或为Null,若判定成立则抛出IllegalArgumentException异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 设置“HashMap阈值”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
this.threshold = tableSizeFor(initialCapacity);
}
//传入一个Map集合,将Map集合中元素Map.Entry全部添加进HashMap实例中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//此构造方法主要实现了Map.putAll()
putMapEntries(m, false);
}
3、JDK1.7 和 JDK1.8 数据结构
1、JDK1.7 (实现方式:数组 + 链表)
HashMap的主干是一个Entry数组,数组中每个元素都是单向链表
HashMap中链表主要是为了解决哈希冲突(两个对象调用hashCode方法计算的哈希值一致导致计算的数组索引值相同)
Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对(Map其实就是保存了两个对象之间的映射关系的一种集合)
JDK1.7官网下载地址
Java Archive Downloads - Java SE 7
修改IDEA中使用的JDK版本
Entry是HashMap中的一个静态内部类;Entry包含4个属性:key,value,hash,next(用于单向链表指向下一个元素地址)
代码如下:
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;
}
JDK1.7 中使用 Entry 数组来存储数据,用key的 hashcode 取模来决定key会被放到数组里的位置,如果 hashcode 相同,或者 hashcode 取模后的结果相同( hash collision ),那么这些 key 会被定位到 Entry 数组的同一个格子里,这些 key 会形成一个链表
在 hashcode 特别差的情况下,如所有key的 hashcode 都相同,这个链表可能会很长,那么 put/get 操作都可能需要遍历这个链表,也就是说时间复杂度在最差情况下会退化到 O(n)
2、JDK1.8 (实现方式:数组 + 链表 + 红黑树)
JDK1.8对HashMap进行了底层优化,JDK1.8对JDK1.7中的HashMap进行了修改,最大的不同就是新增了 红黑树,JDK1.8改为由 数组 + 链表 + 红黑树 实现,主要目的是提高查询效率
JDK1.7中使用 Entry 代表每个HashMap 中数据节点,JDK1.8中使用Node 代表每个HashMap中数据节点(仅仅是换了一个单词没有其他区别),依然还是 key,value,hash,next 四个属性;不过Node仅能用于链表情况,红黑树情况使用 TreeNode(当链表超过8时,链表就转换为红黑树)
Node
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
TreeNode
/**
Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn extends Node) so can be used as extension of either regular or linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
JDK1.8 中使用一个 Node 数组来存储数据,但这个 Node 可能是链表结构,也可能是红黑树结构,如果插入的 key 的 hashcode 相同,那么这些key也会被定位到 Node 数组的同个格子里。如果同一个格子里的key不超过8个(默认阀值),使用链表结构存储。如果超过了8个,那么会调用 treeifyBin 函数,将链表转换为红黑树。
那么即使 hashcode 完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销也就是说put/get的操作的时间复杂度最差只有 O(log n)
3、数组
1、数组优点:
数组是顺序存储结构,通过数组下标可以快速实现对数组元素的访问,效率较高;时间复杂度 O(1)
2、数组缺点:
插入或删除元素效率较低,因为可能需要数组扩容、移动元素
4、链表
1、链表优点:
链表是一种链式存储结构,插入或删除元素不需要移动元素,只需要修改指向下一个节点的指针域,效率较高
2、链表缺点:
链表访问元素需要从头到尾逐个遍历,效率较低;时间复杂度 O(n)
4、二叉树、红黑树 数据结构
1、二叉树查找(二叉树搜索 Binary Search Trees)
可以通过如下网站进行二叉树插入、删除、查询元素过程
二叉树特点:
1、左子树的键值小于根的键值(hash值),右子树的键值大于等于(>=)根的键值
2、对二叉树的节点进行查找时,深度为1 的节点查找次数为1,深度为2 的节点查找次数为2,依次类推......深度为n的节点查找次数为n。查找的时间复杂度依赖于节点深度,如果节点很深,查找效率较低
(1)单向链表
如果按照顺序大小依次插入元素,则会导致二叉树退化为单向链表
(2)查询深度
如果含有较多相同元素,导致深度/高度较深、高,查找效率依然较低
为了提高二叉树的查找效率,引入了一种新的数据结构:平衡二叉树(AV树)
2、平衡二叉树查找(AVL Trees;Balanced binary search trees)
平衡二叉树(AVL树)满足二叉树查找的条件下,还需要满足任何节点的两个子树的高度最大差为1,呈现左右平衡的状态
当向平衡二叉树插入或删除新的节点,会打破原有平衡,会通过旋转恢复平衡
3、红黑树(Red-Black Trees)
红黑树的5个特性:
1、每个节点要么红要么黑
2、根节点为黑
3、如果一个节点是红的,那么它的两个字节点都是黑的
4、从根节点到叶节点或空子节点的每条路径,都包含相同的黑节点
5、每个叶节点(叶节点即树尾端NIL指针或NULL节点)都是黑的
红黑树的查找,插入和删除操作,时间复杂度都是O(logN)
查找操作时与普通的相对平衡的二叉搜索树的效率相同,都是通过相同的方式查找,没有用到红黑树特有的特性。
但如果插入的是有序数据,那么红黑树的查询效率就比二叉搜索树要高了,因为此时二叉搜索树(平衡二叉树)不是平衡树,它的时间复杂度O(N)
插入和删除操作时,由于红黑树的每次操作平均要旋转一次和变换颜色,所以它比普通的二叉搜索树效率要低一点,不过时间复杂度仍然是O(logN)
总之,红黑树的优点就是对有序数据的查均要旋转一次和变换颜色,所以它比普通的二叉搜索树效率要低一点,不过时间复杂度仍然是O(logN)。(红黑树的优点就是对有序数据的查询操作不会慢于O(logN)的时间复杂度)
4、AVL(平衡二叉树)和红黑树(Red-Black Tree)区别
AVL树和红黑树区别AVL树由于维护这种高度(强)平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树。如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL树要优于红黑树
红黑树也是一种平衡二叉树,但每个节点有一个存储位表示节点的颜色,可以是红或黑。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此红黑树是一种弱平衡二叉树,由于是弱平衡,在相同的节点情况下,AVL树的高度<=红黑树,相对于要求严格的AVL树来说,红黑数的旋转次数少,所以对于插入,删除操作较多的情况下,使用红黑树会更好
三、链表和红黑树转换
访问节点方式:先找到节点所在的数组index索引位置,然后判断节点是什么结构进行遍历
节点结构是非树型(链表)结构,通过节点的next遍历链表
节点结构是树型(红黑树)结构,HashMap维护了2种节点之间的联系关系:
1、链表方式:通过节点的next、prev遍历链表
2、红黑树方式:通过根节点root遍历红黑树
链表转换为红黑树之前会判断,链表的节点数量(包括新增节点)大于等于树化阈值即阈值大于等于8时(数组 arr[i] 处存放的链表长度大于8),但数组长度小于64时(数组 arr 长度大),并不会将链表转化为红黑树,而是进行数组扩容
因为数组比较小,尽量避免使用红黑树结构,数组长度小于64时使用红黑树结构反而会降低效率,红黑树要进行左旋,优选,变红黑色来保持平衡。数组长度小于64时,搜索时间相对要快些。可参考 treeifyBin 方法
1、链表->红黑树
//树化阈值为8
static final int TREEIFY_THRESHOLD = 8;
//最小树化容量值为64
static final int MIN_TREEIFY_CAPACITY = 64;
链表转化---->红黑树需要满足2个条件
1、链表的节点数量(包括新增节点)大于等于树化阈值;数组 arr[i] 处存放的链表长度大于8
2、HashMap的容量(Node数组的长度)大于等于最小树化容量值64;数组 arr 长度大于64
1、链表的节点数量(包括新增节点)大于等于树化阈值;数组 arr[i] 处存放的链表长度大于8
HashMap触发判断第一个条件的位置主要有4个方法,分别是putVal方法、computeIfAbsent方法、compute方法、merge方法
1、putVal方法
查看putVal源码可知,根据key判断是否存在节点。若是不存在,则创建节点并加入到HashMap中,返回null。若是存在节点,则直接替换节点的旧值,并返回旧值。
根据key找到在Node数组的index位置,然后若该位置有节点,且节点连接方式是链表,则遍历链表寻找是否有对应key的节点。若是没有,则创建新节点加入到链表中,并判断当前槽位slot的链表的数量(数量包括新增的节点)是否大于树化阈值,链表节点数量大于树化阈值才会进入下一个树化判断,如下图
2、computeIfAbsent方法
computeIfAbsent方法如果根据key找到对应节点,且节点的值不为null,则直接返回旧值,不更新节点的值。如果找不到对应节点或节点的值为null,则根据调用mappingFunction参数的apply方法得到新value,若得到的新value值不为null,则替换掉存在节点的值或者新增值为新value的节点,返回新value。
若是新增节点,且节点连接方式为链表,则判断链表的节点数量(包括新增节点)是否大于等于树化阈值。若是满足,则进入下一个树化条件判断
3、compute方法
compute方法根据调用的remappingFunction参数的apply方法得到新value。若key的节点存在,且新value不为null,则更新节点的值,若新value为null,则删除该节点。若是节点不存在,且新value不为null,新增节点,返回新value。
若是新增节点,且节点连接方式为链表,则判断链表的节点数量(包括新增节点)是否大于等于树化阈值。若是满足,则进入下一个树化条件判断
4、merge方法
merge方法若key的节点存在,且节点值不为null,调用的remappingFunction参数的apply方法得到新value,若节点值为null,则传入的valule为新value。若新value不为null,则更新节点的值,若新value为null,则删除该节点。若是节点不存在,且新value不为null,新增节点,返回新value。
若是新增节点,且节点连接方式为链表,则判断链表的节点数量(包括新增节点)是否大于等于树化阈值。若是满足,则进入下一个树化条件判断
2、HashMap的容量(Node数组的长度)大于等于最小树化容量值64;数组 arr 长度大于64
满足树化第一个条件后,调用treeifyBin方法判断是否满足第二个树化条件。
若HashMap的node数组未初始化或者容量(node数组的长度)小于最小树化容量值,则不会将链表转换为红黑树,而是调用resize方法进行扩容操作。
若是node数组已初始化,且容量大于等于最小树化容量值,则将链表转换为红黑树
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* 链表转化红黑树方法
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
/**
* Forms tree of the nodes linked from this node.
* 红黑树旋转方法
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
2、红黑树->链表
//非树化阈值为6
static final int UNTREEIFY_THRESHOLD = 6;
红黑树转换为链表只需满足以下2个条件之一便可
1、当删除红黑树的节点时,调用removeTreeNode方法。 在removeTreeNode方法中,判断如果根节点root、root.right、root.left、root.lelt.left其中一个为空,则认为该红黑树的节点个数太少了,不必采用红黑树结构,调用untreeify方法将红黑树转化为链表结构
2、红黑树的节点数量小于等于(<=)非树化阈值
1、当删除红黑树的节点时,调用removeTreeNode方法。 在removeTreeNode方法中,判断如果根节点root、root.right、root.left、root.lelt.left其中一个为空,则认为该红黑树的节点个数太少了,不必采用红黑树结构,调用untreeify方法将红黑树转化为链表结构
/**
* Removes the given node, that must be present before this call.
* This is messier than typical red-black deletion code because we
* cannot swap the contents of an interior node with a leaf
* successor that is pinned by "next" pointers that are accessible
* independently during traversal. So instead we swap the tree
* linkages. If the current tree appears to have too few nodes,
* the bin is converted back to a plain bin. (The test triggers
* somewhere between 2 and 6 nodes, depending on tree structure).
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
if (root == null
|| (movable
&& (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
2、红黑树的节点数量小于等于(<=)非树化阈值
当HashMap调用resize方法进行扩容时,如果slot槽位上的节点结构为红黑树,则调用节点的split方法重新分配节点在扩容后新数组的位置
/**
* Splits nodes in a tree bin into lower and upper tree bins,
* or untreeifies if now too small. Called only from resize;
* see above discussion about split bits and indices.
*
* @param map the map
* @param tab the table for recording bin heads
* @param index the index of the table being split
* @param bit the bit of hash to split on
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
在split方法中, 通过(e.hash & bit) == 0(根据key的hash找到数组的位置的一种方式)来判断节点是否在同一个槽位slot
(1)若等于0,则表示该节点在扩容后的新数组的slot位置不变,也就是根据(n-1)&hash(n为扩容后的新数组的长度)找到新数组的index索引等于旧数组的index索引
(2)若是不等于0,则表示该节点在扩容后的新数组的slot位置变了,也就是根据(n-1)&hash(n为扩容后的新数组的长度)找到新数组的index索引改变了,新索引等于旧数组的index索引加上旧数组的长度
通过(e.hash & bit) == 0公式拆分得到的loHead链表和hiHead链表
(1)若是其中链表一个为空,则说明红黑树的所有节点在扩容后的数组index位置相同,则可直接将红黑树移到对应索引位置,不用维护红黑树的结构,因为结构没变化,还是一样的平衡结构
(2)若是都不为空,则说明红黑树的节点分散了,平衡结构被破坏了,需要重新维护红黑树的平衡
(3)拆分后的链表个数若小于等于非树化阈值,说明红黑树的节点个数少了,无需维护红黑树结构,调用untreeify方法将红黑树转化为链表结构
四、实现原理
首先有一个每个元素都是链表的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,同一各链表上的Hash值是相同的,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。
简单来说:
HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。当我们给put(key, value)方法传递键和值时,它先调用key.hashCode()方法,返回的hashCode值,用于找到bucket位置,来储存Entry/Node对象
五、Hash碰撞
如果两个不同的元素,通过哈希函数得出的实际存储地址相同;
也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞
(HashMap 通常会用一个指针数组(table[])来做分散所有的 key,当一个 key 被加入时,会通过 Hash 算法通过 key 算出这个数组的下标 i,然后就把这个 插到 table[i] 中,如果有两个不同的 key 被算在了同一个 i,那么就叫冲突,又叫碰撞,这样会在 table[i] 上形成一个链表)
散列表要解决的一个问题就是散列值的冲突问题,哈希冲突的解决方案有 4 种:
(1)开放定址法
开放定址法就是一旦发生了冲突,就去寻找下一块空的未被占用的散列地址,只要散列表足够大,
空的散列地址总能找到,并将记录存入
(2)链地址法(拉链法)
将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲
突时就把该关键字链在以该单元为头结点的链表的尾部。(将相同hash值的对象组织成一个链表放在hash值对应的槽位)
(3)再哈希法
当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止
(4)建立公共溢出区
将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中
而HashMap即是采用了链地址法,也就是数组+链表的方式
在Java 8 之前,HashMap和其他基于map的类都是通过链地址法解决冲突,它们使用单向链表来存储相同索引值的元素。在最坏的情况下,这种方式会将HashMap的get方法的性能从O(1)降低到O(n)。为了解决在频繁冲突时hashmap性能降低的问题,Java 8中使用平衡树来替代链表存储冲突的元素。这意味着我们可以将最坏情况下的性能从O(n)提高到O(logn)。
在Java 8中使用常量TREEIFY_THRESHOLD来控制是否切换到平衡树来存储。目前,这个常量值是8,这意味着当有超过8个元素的索引一样时,HashMap会使用树来存储它们
HashMap的数组长度为什么是2的n次幂
在get方法和put方法中都需要先计算key映射到哪个桶上,然后才进行之后的操作,计算的主要代码如下:
(n - 1) & hash
(1)&(按位与运算):相同二进制数位上,都为1,结果为1,否则为0
(2)^ (按位异或运算):相同二进制数位上,数字相同,结果为0,不同为1
举例说明如下
通过上边可以看到,当数组长度不为2的n次幂 的时候,hashCode 值与数组长度减一做与运算的时候,会出现重复的数据,因为不为2的n次幂 的话,对应的二进制数肯定有一位为0,这样,不管你的hashCode 值对应的该位,是0 还是1 ,最终得到的该位上的数肯定是0 ,这带来的问题就是HashMap 上的数组元素分布不均匀,而数组上的某些位置,永远也用不到
这将带来的问题就是HashMap 数组的利用率太低,并且链表可能因为上边的(n - 1) & hash 运算结果碰撞率过高,导致链表太深。(当然jdk 1.8已经在链表数据超过8个以后转换成了红黑树的操作,但那样也很容易造成它们之间的转换时机的提前到来)
所以说,当数组长度为2的n次幂的时候,数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了
六、扩容时重哈希(rehash)
1、get方法
实现步骤大致如下:
1、通过 hash 值获取该 key 映射到的桶
2、桶上的 key 就是要查找的 key,则直接命中
3、桶上的 key 不是要查找的 key,则查看后续节点:
(1)如果后续节点是树节点,通过调用树的方法查找该 key。
(2)如果后续节点是链式节点,则通过循环遍历链查找该 key。
/**
* get方法主要调用的是getNode方法
*/
public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K, V> getNode(int hash, Object key) {
Node<K, V>[] tab, first, e;
int n; K k;
// 如果哈希表不为空&&key对应的桶上不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 是否直接命中
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 判断是否有后续节点
if ((e = first.next) != null) {
// 如果当前的桶是采用红黑树处理冲突,则调用红黑树的get方法去获取节点
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
// 不是红黑树的话,采用传统的链式结构,通过循环的方法判断链中是否存在该key
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
2、put方法
put 方法比较复杂,实现步骤大致如下:
1、先通过 哈希函数 获取Key对应的hash 值,计算出 key 映射到哪个桶,计算其数组下标
2、如果桶上没有发生碰撞出现哈希冲突,则直接插入数组;如果发生碰撞出现哈希冲突,则需要处理哈希冲突:
(1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入
(2)否则采用传统的链式方法插入。如果链的长度到达临界值,则把链转变为 红黑树
3、如果链表长度超过阀值(TREEIFY THRESHOLD==8),就把链表转成红黑树
4、如果桶中存在重复的键key,则为该键替换新值value
5、如果 size 大于阈值,则调用resize方法进行数组扩容
/**
*put 方法的具体实现也是在 putVal 方法中,所以我们重点看下面的 putVal 方法
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
hash(key):hash key的hash值
key:原始key值
value:存放的值
onlyIfAbsent:true代表不更改现有值
evict:false表示table为创建状态
*/
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;
//如果桶上节点的key与当前key重复,则就是要找的节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果是采用红黑树的方式处理冲突,则通过红黑树的putTreeVal方法去插入这个键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
else {//否则就是传统的链式结构
//采用循环遍历的方式,判断链中是否有重复的key
for (int binCount = 0; ; ++binCount) {
//到了链尾还没有找到重复的key,则说明HashMap没有包含该键
if ((e = p.next) == null) {
// 创建一个新节点插入到尾部
p.next = newNode(hash, key, value, null);
// 如果链的长度大于TREEIFY_THRESHOLD这个临界值,则把链变成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了重复的key
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;
}
3、hash方法
哈希表底层采用 key的hashCode方法值结合数组长度进行无符号右移(>>>),按位异或(^),按位与(&)计算索引;位运算效率较高
还可采用方法计算:平方取中法、取余数、伪随机数
在get方法和put方法中都需要先计算key映射到哪个桶上,然后才进行之后的操作,计算的主要代码如下:
(n - 1) & hash
上面代码中的n指的是哈希表的大小,hash指的是key的哈希值
hash是通过下面这个方法计算出来的,采用了二次哈希的方式,其中key的hashCode方法是一个native方法:
采用高位16位组成的数字 与 源哈希值 取异或 而 生成的哈希值 作为 用来计算 HashMap 的数组位置的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后再通过putVal方法计算上述hash方法计算得到的哈希值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
......
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
......
}
(1) key.hashCode():返回哈希值也就是hashcode
(2)n:数组初始化长度16
(3)&(按位与运算):相同二进制数位上,都为1,结果为1,否则为0
(4)^ (按位异或运算):相同二进制数位上,数字相同,结果为0,不同为1
举例说明如下
使用异或:首先,对于一个数字,转换成二进制之后,其中为的 1 的位置代表这个数字的特性
对于异或运算,如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。0与0异或是0,0与1异或是1,这样相当于让高位的特性在低位得以体现,所以采用这种算法,减少碰撞。
这个hash方法先通过key的hashCode方法获取一个哈希值,再拿这个哈希值与它的高16位的哈希值做一个异或操作来得到最后的哈希值,注释中是这样解释的:如果当n很小,假设为64的话,那么n-1即为63(0x111111),这样的值跟hashCode()直接做与操作,实际上只使用了哈希值的后6位。如果当哈希值的高位变化很大,低位变化很小(此时hash值基本不会变化),这样就很容易造成冲突了,所以这里把高低位都利用起来,从而解决了这个问题
正是因为与的这个操作,决定了HashMap的大小只能是2的幂次方,想一想,如果不是2的幂次方,会发生什么事情?即使你在创建HashMap的时候指定了初始大小,HashMap在构建的时候也会调用下面这个方法来调整大小:
static final int tableSizeFor(int cap) {
int n = cap - 1;
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;
}
这个方法的作用看起来可能不是很直观,它的实际作用就是把cap变成第一个大于等于2的幂次方的数。例如,16还是16,13就会调整为16,17就会调整为32
4、resize方法rehash
HashMap在进行扩容时,使用的rehash方式非常巧妙;因为每次扩容都是翻倍,与原来计算(n-1)& hash 的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到“原位置 + 旧容量”这个位置
HashMap进行扩容时,不需要重新计算hash值,只需要看原来的hash值新增的bit位是1还是0,是0索引位置不变,是1则索引变成“原位置 + 旧容量”这个位置
例如,原来的容量为32,那么应该拿hash跟31(0x11111)做与操作;在扩容扩到了64的容量之后,应该拿hash跟63(0x111111)做与操作。新容量跟原来相比只是多了一个bit位,假设原来的位置在23,那么当新增的那个bit位的计算结果为0时,那么该节点还是在23;相反,计算结果为1时,则该节点会被分配到23+31的桶上
总结:
1、计算新的索引高位是0,则存储到 原来索引位置
2、计算新的索引高位是1,则存储到 原来索引+旧数组长度 位置
正是因为这样巧妙的rehash方式,保证了rehash之后每个桶上的节点数必定小于等于原来桶上的节点数,即保证了rehash之后不会出现更严重的冲突
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//计算扩容后的大小
if (oldCap > 0) {
//如果当前容量超过最大容量,则无法进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//没超过最大值则扩为原来的两倍
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);
}
//新的resize阈值
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)
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;
//通过上面讲的方法来计算节点的新位置
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;
}
七、HashMap的key一般用字符串,能用其他对象吗?
HashMap是key-value(键值对)组成的,这个key既可以是基本数据类型对象,如Integer,Float,同时也可以是自己编写的对象
为何key一般用字符串呢?
在《Java 编程思想》中有这么一句话:设计 hashCode() 时最重要的因素就是对同一个对象调用 hashCode() 都应该产生相同的值
String 类型的对象对这个条件有着很好的支持,因为 String 重写了 hashCode()方法,它的 hashCode() 值是根据 String 对象的内容计算的,并不是根据对象的地址计算, 所以内容相同的 String 对象会产生相同的散列码
HashMap的key可以用其他对象,但是相较String对象而言所需要的条件较为苛刻。即使自定义对象内部重写了hashCode()方法和equals()方法,也没有String对象高效,因为String是不可变对象,其中有个hash变量,它可以缓存hashCode,避免重复计算hashCode,这样查找更快。
八、重写equals方法需同时重写hashCode方法
Java中规定:
(1)如果两个对象通过equals方法比较是相等的,那么它们的hashCode方法结果值也是相等的
(2)如果两个对象通过hashCode方法结果值是相等的,它们的equals方法比较不一定相等
Java就采用了hash表,利用哈希算法(也叫散列算法),将对象数据根据该对象的特征使用特定的算法将其定义到一个地址上,那么在后面定义进来的数据只要看对应的hashcode地址上是否有值,那么就用equals比较,如果没有则直接插入,这样就大大减少了equals的使用次数,执行效率就大大提高了
为了提高效率,采取重写hashcode方法,先进行hashcode比较,如果不同,那么就没必要在进行equals的比较了,这样就大大减少了equals比较的次数,这对比需要比较的数量很大的效率提高是很明显的
Object类中的equals方法,底层是通过 == 来进行比较,所以比较的是对象的内存地址;很多情况下,我们比较对象并不需要比较对象的地址,而是只要是同一个类的不同对象,成员属性值相同,我们就认为是同一个对象,为了达到这个效果,就必须重写equals方法,让他们比较的是类的类型和成员变量的值
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题
在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)
九、线程不安全
1、HashMap 在并发时可能出现的问题主要是两方面:
(1)如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
(2)如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失
2、实现线程安全的三种方式:
1、HashTable
2、Collections.synchronizedMap()
3、ConcurrentHashMap
1、HashTable
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
......
public synchronized V put(K key, V value)
public synchronized V get(Object key)
......
}
HashTable的put操作使用synchronized
关键字修饰来保证线程安全,但是所有线程竞争同一把锁,效率低
2、Collections.synchronizedMap()
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
/**
* @serial include
*/
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L;
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
}
调用synchronizedMap()方法后会返回一个SynchronizedMap类的对象,而在SynchronizedMap类中使用了synchronized同步关键字来保证对Map的操作是安全的
3、ConcurrentHashMap(推荐使用,效率较高)
ConcurrentHashMap JDK1.7中采用了分段锁技术,将数据分段存储,给每一段数据配一把锁,效率高.其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理
JDK1.8中放弃了 Segment 分段锁,采用了CAS + synchronized来保证线程安全
ConcurrentHashMap通过 CAS+synchronized实现线程安全
Java8中使用CAS算法(CAS算法请参考 Java并发编程之原子变量和CAS算法)
1、ConcurrentHashMap在JDK 1.7中使用的数组 加 链表的结构,其中数组分为两类,大树组Segment 和 小数组 HashEntry,而加锁是通过给Segment添加ReentrantLock重入锁来保证线程安全的
2、ConcurrentHashMap在JDK1.8中使用的是数组 加 链表 加 红黑树的方式实现,它是通过 CAS 或者 synchronized 来保证线程安全的,并且缩小了锁的粒度,查询性能也更高
添加元素时首先会判断容器是否为空,如果为空则使用volatile 加CAS 来初始化,
如果容器不为空,则根据存储的元素计算该位置是否为空
如果根据存储的元素计算结果为空则利用 CAS 设置该节点;
如果根据存储的元素计算不为空,则使用 synchronized,然后遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树。这样就能保证并发访问时的线程安全了
简而言之,就是ConcurrentHashMap通过对头结点加锁来保证线程安全的
这样设计的好处是,使得锁的粒度相比Segment来说更小了,发生hash冲突 和 加锁的频率也降低了,在并发场景下的操作性能也提高了。而且,当数据量比较大的时候,查询性能也得到了很大的提升
final V putVal(K key, V value, boolean onlyIfAbsent) {
//与HashMap不同,ConcurrentHashMap不允许null作为key或value
if (key == null || value == null) throw new NullPointerException();
//计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh;
//若table为空的话,初始化table
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//若当前数组i位置上的节点为null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//CAS插入节点(V:当前数组i位置上的节点; O:null; N:新Node对象)
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//当前正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//锁住当前数组i位置上的节点
synchronized (f) {
//判断是否节点f是否为当前数组i位置上的节点,防止被其它线程修改
if (tabAt(tab, i) == f) {
//当前位置桶的结构为链表
if (fh >= 0) {
binCount = 1;
//遍历链表节点
for (Node e = f;; ++binCount) {
K ek;
//若hash值与key值相同,进行替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
//若链表中找不到,尾插节点
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
//当前位置桶结构为红黑树,TreeBin哈希值固定为-2
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
//遍历红黑树上节点,更新或增加节点
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//若链表长度超过8,将链表转为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//节点数+1,若超过阈值则扩容
addCount(1L, binCount);
return null;
}
/**
* hash算法
*/
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
CAS操作
concurrentHashMap调用Unsafe类中的方法实现CAS(这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作),其内部方法大多为native方法即直接调用操作系统底层资源执行相应任务,提供了一些可以直接操控内存和线程的底层操作
十、HashMap与HashTable、 ConcurrentHashMap的区别
1、HashMap
底层结构 数组+链表+红黑树,可以存储null、键null值,线程不安全;
初始size为16,扩容:newSize = oldSize * 2,size一定为2的n次幂(元素分配更均匀)
计算index方法:index = hash & (tab.length – 1)
2、HashTable
底层结构 数组+链表,无论是key还是value都不能为null,线程安全。
实现线程安全的方式是在修改数据时锁住整个hashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,扩容:newSize = oldSize * 2 + 1;实现原理和HashMap类似
3、ConcurrentHashMap
底层结构 数组+链表,线程安全,效率高
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
参考链接
Java--HashMap 在高并发下引起的死循环_MinggeQingchun的博客-CSDN博客
Java--Java集合之一—HashMap_MinggeQingchun的博客-CSDN博客
Java中的HashMap详解_总结经验,与君共勉-CSDN博客_hashmap详解
Java并发——ConcurrentHashMap(JDK 1.8) - 掘金
ConcurrentHashMap是如何保证线程安全的?_Tom弹架构的博客-CSDN博客_concurrenthashmap如何保证线程安全