文章目录
1.HashMap
HashMap是面试高频考点,所以深入了解一下hashmap,对hashmap有更深了解
hashmap是Java中的集合类,他的底层是数组+链表
从上图可以看出,hashmap是数组+链表的结构,当我们new一个hashmap时,初始化了一个数组。
hashmap中的重要参数
// 默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 16;
// 最大容量 2^30
static final int MAXIMUM_CAPACITY = 1073741824;
// 默认装载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75F;
// 树阈值,如果链表长度大于8,链表转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 由树转化为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树)
// 如果哈希表的容量 < 该值,直接扩容,不转为红黑树
// 这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
// hashmap中的数组结构
transient HashMap.Node<K, V>[] table;
transient Set<Entry<K, V>> entrySet;
// hashmap当前的node数量
transient int size;
// hashmap修改次数
transient int modCount;
// 当hashmap中的元素数量超过这个值则扩容
int threshold;
// 扩容因子大小
final float loadFactor;
1)hashmap中的put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 如果表为空或表的长度为0,则用resize方法初始化表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 根据hash得到表中索引位置的桶,如果桶为空,则直接将节点node插入到桶中
// 此时p已经指向了索引位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果桶不为空
else {
Node<K,V> e; K k;
// 判断桶的第一个节点的key和待插入的key是否相同,如果相同,把p赋值给e
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 {
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;
}
// 判断p的下一个节点的key是否和传入的key相等,如果相等,e的值不为空,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e不为空,表示链表中有相同的key,用新值替换旧值,并换回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// map修改次数+1
++modCount;
// 如果超过最大节点数,则扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2)hashmap中的get()方法
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
Node<K,V> first, e;
// 数组长度
int n;
// 当前指向节点的key
K k;
// 表不为空 && 表长度不为0 && 待查找的桶不为空
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;
}
2.HashTable
HashTable和HashMap相似,但又有些不同。HashTable和HashMap一样,都是基于哈希表构建的,同样是用Key-Value的形式存储键值对,内部同样是用单链表解决冲突的,并且当元素超过一定值时,同样会进行自动扩容。
HashMap和HashTable的不同:
1)继承父类不同
HashMap是继承AbstractMap类的,而HashTable继承Dictionary类,但二者都实现了Map接口
2)线程安全性不同
HashMap是线程不安全的,因为HashMap中维护的是一个Entry数组,当发生hash冲突时,HashMap采用单链表的方式来解决hash冲突,在对应的位置放入链表的头结点。
当数据存储的时候,先根据key的hash找到对应Entry数组的链表,再获取到链表尾部,再将数据写入到链表尾部。在单线程环境下当然可以很好的工作,但是如果在多线程环境下就会有线程不安全的问题,例如线程a和线程b同时往HashMap中添加元素,他们同一时间获取到了HashMap的尾部节点,然后a先将数据写入,b后将数据写入,此时b的数据将会覆盖a的数据,产生线程不安全的问题。
HashTable中的方法是synchronized的,可以直接在多线程的环境下使用。
如果想让hashmap同样变成synchronized的,可以使用Collections类中的synchronizedMap方法:
Map map = Collections.synchronizedMap(new HashMap<>()…);
3)是否提供contains方法
HashMap去掉了contains方法,改成了containsKey和containsValue
HashTable保留了contains、containsKey和containsValue方法,其中contains和containsValue效果一样
4)是否支持null作为key或value
HashTable不支持null作为key或value,当HashTable有put(null,null)的操作时,可以通过编译,但是运行时会报出NullPointerException异常。
HashMap支持null作为key或value,但是作为null的key只能有一个,而作为null的value可以有多个,所以不能用get()的方法判断存在某个键,而应该用containsKey方法。
5)遍历内部不同
HashTable和HashMap都使用了Iterator,但由于历史原因,HashTable还使用了Enumeration的方式
6)Hash值不同
HashTable计算key的hash值时,直接用key的hashCode方法,而HashMap重新计算hash值
Hashtable计算hash值,直接用key的hashCode(),而HashMap重新计算了key的hash值,Hashtable在求hash值对应的位置索引时,用取模运算,而HashMap在求位置索引时,则用与运算,且这里一般先用hash&0x7FFFFFFF后,再对length取模,&0x7FFFFFFF的目的是为了将负的hash值转化为正值,因为hash值有可能为负数,而&0x7FFFFFFF后,只有符号外改变,而后面的位都不变。
7)内部使用的数组初始化大小和扩容方式不同
HashMap初始化大小为16,HashTable初始化大小为11
HashMap扩容时,扩容为原来的两倍;HashTable扩容时,扩容为原来的两倍+1
3.ConcurrentHashMap
CurrentHashMap也是一种Map,用来存储键值对。不同于HashMap是线程不安全的,也不同于HashTable的方法都是用Synchronized修饰 。它提供了一种无锁化的机制来解决问题,实现就是Compare And Swap :CAS,属于乐观锁的一种,它可以保证对某个操作的线程安全。它是怎么保证的呢?—>它会去拿到对某个数据操作的内存当中的最新值以及你需要去修改的这个值,两者进行比较,如果说是相等的,就代表是安全的没有被改动过,如果不相等,就认为被其他线程改动过,这时候就不能再对它进行改动。
下面我们结合源码来看一下CurrentHashMap具体怎么保证线程安全的
1)初始化数组:initTable
首先我们要知道这么一个变量sizeCtl,其的外部定义如下
private transient volatile int sizeCtl;
这个变量是CAS无锁化机制的保证,我们就是根据这个变量的值来判断数据有没有被改动。这个变量初始值为0
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 当table为空或table长度为0时,开始初始化数组
while ((tab = table) == null || tab.length == 0) {
// 如果sizeCtl这个值<0时,线程让步(即已经有线程在initTable了)
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
/**
* 第一个进入的线程开始initTable
* compareAndSwapInt这个方法的意思是
* 在当前对象中,判断sc和sizeCtl的值是否相等
* 如果相等,将sizeCtl的值设置为-1并返回true
* 如果不相等,返回false
*/
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
// DEFAULT_CAPACITY 数组的默认容量是16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 将这个数组赋值给table,table是volatile的
table = tab = nt;
// 设置sc为0.75 * n
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
当第一个线程进入以后,sizeCtl被修改成-1,此后进入的线程因为sizeCtl < 0,所以只能让步。
2)put()方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 得到hash值
int hash = spread(key.hashCode());
// 记录链表长度
int binCount = 0;
// 遍历
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果数组为空,进行数组初始化,initTable上面讲过了
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 找到hash值对应的节点下标,得到第一个节点f
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果该节点为空,进行一次CAS操作
// 如果CAS操作失败,说明有并发操作,进行到下一个循环
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// hash值等于MOVED,这个后面讲
else if ((fh = f.hash) == MOVED)
// 进入这里,说明有其他线程正在把该链表进行扩容
// 扩容非常耗时,所以使用helpTransfer,利用该线程帮助扩容
tab = helpTransfer(tab, f);
// f是该位置的头结点且不为空
else {
V oldVal = null;
// 获取头结点的锁
synchronized (f) {
if (tabAt(tab, i) == f) {
// 头结点的哈希值大于0,说明是链表
if (fh >= 0) {
binCount = 1;
// 遍历链表,用bigCount记录链表长度
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 如果发现相等的key,判断是否需要值覆盖,然后结束
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;
}
}
}
// 如果是树结构
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;
}
}
}
}
// 如果bigCount不等于0,说明在做链表操作
if (binCount != 0) {
// 如果链表深度超过默认值8,则进行红黑树转化或扩容
if (binCount >= TREEIFY_THRESHOLD)
// 扩容或转化为红黑树,这个方法后面会讲
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
在这里还有两个问题,一个是红黑树转化或扩容方法treeifyBin(tab, i),和帮助扩容函数helpTransfer(tab, f)
3)链表转红黑树treeifyBin(tab, i)
前面我们说过了,在链表长度大于8时,会链表会自动转化为红黑树,但和hashmap不同的是,ConcurrentHashMap不仅会转化会红黑树,还有可能扩容(当长度小于64时),下面我们看看源码
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
// 如果小于64时,即链表长度为32、16或更小时,进行数组扩容
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)、
// 后面我们会介绍这个方法
tryPresize(n << 1);
// b是头结点
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
// 给b加锁
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
// 遍历链表,建立一棵红黑树
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
// 将红黑树加入到数组相应位置
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
4)链表扩容方法tryPresize(int size)
上面我们说过,在treeifyBin(tab, i)方法中,如果链表长度小于64时,会进行数组扩容,我们结合源码来看看
// 首先说明,size在传入的时候,已经翻了倍了
private final void tryPresize(int size) {
// c:size的1.5倍,再+1,再往上取最近的2的次方
int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
tableSizeFor(size + (size >>> 1) + 1);
int sc;
while ((sc = sizeCtl) >= 0) {
Node<K,V>[] tab = table; int n;
// 这个分支和之前的initTable分支基本一样
// 这个分支主要是为了putAll()方法提供的
if (tab == null || (n = tab.length) == 0) {
n = (sc > c) ? sc : c;
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if (table == tab) {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
}
}
else if (c <= sc || n >= MAXIMUM_CAPACITY)
break;
else if (tab == table) {
int rs = resizeStamp(n);
if (sc < 0) {
Node<K,V>[] nt;
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
}
}
}
5)数据迁移方法
这个方法很长,将原来的table数组转移到newTab数组中。
该方法是一个多线程方法,因为会有很多任务同时调用该方法。
首先,我们得理解这个transfer方法的工作机制,以及为什么其他线程可以帮助该线程完成工作。
例如现在的数组长度为n,所以此时我们有n个迁移任务,让每个线程负责一小个任务是最简单的,在每次做完一个任务以后再查看是否还有任务没有做完,如果还有任务没有做完则继续做下一个任务。此时我们定义了一个变量stride,就是步长。如果每次迁移16个任务,我们就需要一个全局的调度者来调度哪几个线程迁移哪几个任务,这就是属性transferIndex 的作用
第一个发起数据迁移的线程会将transferIndex迁往数组最后,然后从后往前的stride属于第一个线程,同时将transferIndex指向新的位置,然后再往前的stride属于第二个线程,再将transferIndex指向新的位置,以此类推,知道完成迁移任务。当然这里的第一个线程第二个线程不一定是不同线程,也有可能是同一线程,即第一个线程任务做完后,继续做第二个任务。
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}