文章目录
第一章 先分别说明
1.1 HashMap
1.1.1 HashMap介绍
- 可以存储null键和null值,线程不安全
- 在JDK1.7之前HashMap是由数组+链表构成的
- 在JDK1.8之后HashMap则由数组+链表+红黑树构成
- 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
- 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
- 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
- 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
- 计算index方法:index = hash & (tab.length – 1)
1.1.2 HashMap的初始值还要考虑加载因子
- 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
- 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
- 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。
1.1.3 HashMap源码
继承机制:
/*
* @see Collection
* @see Map
* @see TreeMap
* @see Hashtable
* @since 1.2
*/
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
}
扩容机制:
/**
* 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.
*
* @return the table
*/
final Node<K,V>[] resize() {
//初始化的table赋值
Node<K,V>[] oldTab = table;
//判断table是否初始化了,如果没有初始化则oldCap置为1,否则是初始化的长度
//put前的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//threshold-阈值,超过阈值会进行扩容 初始阈值为0
//put前的阈值
int oldThr = threshold;
int newCap, newThr = 0; 定义新容量新阈值
//put前已经有初始容量
if (oldCap > 0) {
//如果put前容量 >= 最大容量2的30次幂
if (oldCap >= MAXIMUM_CAPACITY) {
//阈值扩大到最大的int值 2^31-1
threshold = Integer.MAX_VALUE;
return oldTab;//返回put前的node[]
}
//如果是之前的容量扩大到2倍之后还小于map最大容量 并且 之前的容量超过初始容量16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新的阈值 为 老的阈值的两倍
newThr = oldThr << 1; // double threshold
}
//put前的阈值大于0
else if (oldThr > 0)
newCap = oldThr; // 初始容量被置于阈值
else {
//零初始阈值表示使用默认值
//16 初始容量
newCap = DEFAULT_INITIAL_CAPACITY;
//(0.75*16)初始阈值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新阈值为0
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
//新阈值=如果新容量<最大容量 并且阈值也小于最大容量则为当前容量*加载因子否则为最
//大的int值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//修改当前map阈值为新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"}) //禁止显示警告
//new一个新的长度为新容量的node[]
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//修改当前map中node[]
table = newTab;
//老的table存在
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;
}
}
}
}
}
//返回这个重新构建的node[]
return newTab;
}
1.2 HashTable
1.2.1 HashTable介绍
- 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
- 初始size为11,扩容:newsize = olesize*2+1
- 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
1.2.2 HashTable源码
继承机制:
/*
* @see Hashtable#rehash()
* @see Collection
* @see Map
* @see HashMap
* @see TreeMap
* @since JDK1.0
*/
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
}
扩容机制:
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 新数组的容量=旧数组长度*2+1
int newCapacity = (oldCapacity << 1) + 1;
// 保证新数组的大小永远小于等于MAX_ARRAY_SIZE
// MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
return;
newCapacity = MAX_ARRAY_SIZE;
}
// 创建新数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
// 计算新的临界值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 将旧数组中的元素迁移到新数组中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
//计算新数组下标
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
// 头插法的方式迁移旧数组的元素
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
1.3 ConcurrentHashMap
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
1.3.1 ConcurrentHashMap介绍
- jdk1.7底层采用分段的数组+链表实现,线程安全
- jdk1.8底层采用分段的数组+链表+红黑树实现,线程安全
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 读取数据时不加锁,高效,且因为map中的value值是添加volatile关键字修饰的,可保证读取到最新值,降低CPU负载。
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
1.3.2 ConcurrentHashMap源码
继承机制:
/*
* @since 1.5
* @author Doug Lea
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
}
扩容机制:
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容器并放入新容器
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;
//【第二步,划分槽位,帮助推进】
//选择当前线程进行transfer的槽位,从最后一个槽位向前
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;
//设置下次扩容的临界点为 0.75*扩容容量
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重置,在看下还有没有transferIndex
//如果已经是唯一处理线程并且满足前置条件,为何需要检查下?
i = n; // recheck before commit
}
}
//【第四步,处理槽位】
//如果当前槽中没有成员,用forwarding节点占位
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//如果当前槽中成员为forwarding节点,代表已经被处理过了
else if ((fh = f.hash) == MOVED)
//处理下一个槽
advance = true; // already processed
else {
//锁住槽位
synchronized (f) {
//double check
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
//计算当前成员最高位
//runBit是0 or 1
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;
//查找最后重复的链,获得开始位置p,和重复的高位值runBit
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//如果从p开始后面高位全是0,那么就不需要移动到新槽中
if (runBit == 0) {
ln = lastRun;
hn = null;
}
//如果从p开始后面全是1,那么就需要移动到新槽中
else {
hn = lastRun;
ln = null;
}
//从链的头部一直遍历到p的位置(因为p以后高位都一样)
//为何需要提前找一部分重复?效率更高?这么处理是否有理论依据?
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
//高位为0放到旧槽位中
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
//高位为1放到新槽位中
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//将ln放到新容器的旧槽位中
setTabAt(nextTab, i, ln);
//将hn放到新容器的新槽位中
setTabAt(nextTab, i + n, hn);
//将老容器中的该节点设置为forwarding节点
setTabAt(tab, i, fwd);
//处理下一个槽位
advance = true;
}
//TreeBin的hash固定为-2,红黑树的调整
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;
}
}
//槽位里成员少于等于6,退化为链表
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;
}
}
}
}
}
}
第二章 总体来分析三者(一个表格)
HashMap | HashTable | ConcurrentHashMap | |
---|---|---|---|
null键 | 允许 | 不允许 | 不允许 |
null值 | 允许 | 不允许 | 不允许 |
效率 | 非常高 | 低 | 高 |
线程安全 | 不安全 | 安全 | 安全 |
数据结构 | jdk1.8 数组+链表+红黑树 | 数组+链表 | jdk1.8 数组+链表+红黑树 |
同步方式 | 无 | synchronized同步方法 | 1.7版本:基于segment分段锁机制,基于ReentrantLock实现;1.8版本:基于CAS+synchronized实现,空节点插入使用CAS,有Node节点则使用synchronized加锁 |
迭代器类型 | fail-fast迭代器:在遍历时不断更新元素,否则将抛出异常 | fail-safe迭代器:基于容器的克隆,因此遍历操作时元素的更新不影响遍历 | fail-safe迭代器:基于容器的克隆,因此遍历操作时元素的更新不影响遍历 |
总结:不涉及线程安全问题时使用HashMap,要保证线程安全时,使用ConcurrentHashMap。