Skip to the content.

首页

HashMap & ConcurrentHashMap


class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

属性

transient Node<K,V>[] table; // Node数组,默认大小16,需要为2的幂次方。

transient int size; // 大小

final float loadFactor; // 装载因子,允许自定义,默认为0.75。

int threshold; // 容量阈值,为tableSize * loadFactor。

transient int modCount; // 并发Fail-Fast机制

transient Set<Map.Entry<K,V>> entrySet; // EntrySet代理对象缓存

静态方法

实例方法

Node操作

final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>

红黑树,为2-3-4树,叶子节点可以为4节点,包含parent、left、right、prev以及next(Node)指针。

实例方法

静态方法


class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable

ConcurrentHashMap为线程安全的HashMap,同样是使用拉链法,键和值都不支持为null;相比于JDK8之前的版本,不再使用分段锁,而是在单个槽位上同步,不再支持设置loadFactor和concurrencyLevel。

属性


transient volatile Node<K,V>[] table; // 当前哈希表,使用Unsafe安全操作。

private transient volatile Node<K,V>[] nextTable; // 新哈希表,扩容过程使用

// 1、创建时为哈希表初始容量,0则使用默认容量16
// 2、初始化时为-1
// 3、扩容时小于-1,高16位为扩容标记,低16位值为辅助扩容的线程数+1(1表示扩容已结束)
// 4、其他阶段为扩容阈值(HashMap的threshold属性),loadFactor固定为0.75
private transient volatile int sizeCtl;

// 并发转移的下一个区段的起点,从右往左移动,这样只需要与0比较即可,0表示所有槽位已被转移完成。
private transient volatile int transferIndex;

// 用于计数,实现同LongAdder。
private transient volatile long baseCount; // 计数基准值
private transient volatile int cellsBusy; // 计数单元初始化及扩容时使用
private transient volatile CounterCell[] counterCells; // 竞争场景计数单元

// 视图,为代理对象。
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;

静态方法

Nodes

Map方法

初始化 & 扩容


问题

tableSize为什么需要是2的幂次方?

  1. hashcode % tableSize == hashcode & (tableSize - 1),哈希槽的确定可以用一次&运算来替代取余运算;
  2. 扩容时哈希槽上的链表只需要被拆分为两条,一条设置到当前位置i,另一条设置到i + tableSize位置。

链表的长度可能大于8吗?

可能!哈希表长度小于64时并不会将链表升级为红黑树而是扩容一次,而扩容后链表虽然会被拆分为两条,但长度仍然可能大于8。

红黑树的节点数可能小于6个吗?

可能!只有拆分红黑树时才是通过统计结点个数来判断是否需要退化,节点移除操作是通过特定的树型进行判断(红黑树的特性决定其可以通过树型判断节点数是否可能过小),也可能导致某些节点数小于6个的树型不会触发退化;同时使用迭代器移除时也不会触发红黑树退化(为了不破坏迭代过程)

ConcurrentHashMap效率高在哪里?

  1. 相比于分段锁,加锁粒度更小,从使用ReentrantLock锁多个槽变为使用同步锁锁单个槽;
  2. 支持辅助扩容,且不阻塞get操作。

不加锁的读操作为什么是线程安全的?

  1. 如果是普通Node节点则直接遍历查找;如果查找中发生了升级,因为红黑树升级并不会破坏原链表顺序故不会造成影响;如果发生了插入,插入是尾插法不会影响;如果发生了移除,移除并不会修改next指针,遍历仍可以继续;如果发生了替换,只会更新节点的值,不会造成影响。
  2. 如果是扩容节点,会通过转移节点进入最新的哈希表内查找,由于已经设置了转移节点,即该槽位已经转移完成,故不会影响查询操作。
  3. 如果是TreeBin节点,通过其读写锁机制保证。
  4. 如果是保留节点,表示当前计算还未完成,且当前槽之前为空,直接返回null。

保留节点的问题?

如果计算操作为阻塞型操作,则会导致其他线程因为等待该保留节点的同步锁而阻塞,因为put等其他操作是先获取到同步锁之后才判断节点是否是保留节点,所以无法做到快速失败

TreeBin为什么还需要额外的加锁机制?

因为读操作是不会对槽加锁的,但红黑树写操作会改变红黑树结构,为了读写操作的互不影响,需要额外的读写锁机制。


ConcurrentHashMap解析