Prefácio
No desenvolvimento diário, costumamos usar HashMap de pares chave-valor, que é implementado usando uma tabela hash, trocando espaço por tempo e melhorando o desempenho da consulta.
Mas em cenários simultâneos multithread, o HashMap não é seguro para threads.
Se quiser usar segurança de thread, você pode usar ConcurrentHashMap, HashTable, Collections.synchronizedMap, etc.
No entanto, como a granularidade do uso de sincronizado é muito grande para os dois últimos, eles geralmente não são usados. Em vez disso, é usado ConcurrentHashMap no pacote simultâneo.
No ConcurrentHashMap, o volátil é usado para garantir a visibilidade da memória, para que não haja necessidade de "travar" para garantir a atomicidade nos cenários de leitura.
Use CAS + sincronizado em cenários de gravação. Sincronizado bloqueia apenas o primeiro nó em uma determinada posição de índice na tabela hash, o que equivale ao bloqueio refinado e aumenta o desempenho da simultaneidade.
Este artigo analisará o uso de ConcurrentHashMap, os princípios de implementação de leitura, escrita e expansão e ideias de design.
Antes de ler este artigo, você precisa entender tabelas hash, voláteis, CAS, sincronizadas, etc.
Para voláteis, você pode conferir este artigo: 5 casos e fluxogramas para ajudá-lo a entender a palavra-chave volátil de 0 a 1
Para CAS e sincronizado, você pode visualizar este artigo: 15.000 palavras, 6 casos de código e 5 diagramas esquemáticos para ajudá-lo a entender completamente o Sincronizado
Usar ConcurrentHashMap
ConcurrentHashMap é um mapa thread-safe em cenários simultâneos. Ele pode consultar e armazenar pares de valores-chave K e V em cenários simultâneos.
Objetos imutáveis são absolutamente seguros para threads, independentemente de como são usados pelo mundo exterior.
ConcurrentHashMap não é absolutamente seguro para threads. Ele apenas fornece segurança de thread para métodos. Se usado incorretamente na camada externa, ainda causará insegurança de thread.
Vejamos o seguinte caso. Use valor para armazenar o número de chamadas de incremento automático, inicie 10 threads e execute cada uma delas 100 vezes. O resultado final deve ser 1.000 vezes, mas o uso incorreto resulta em menos de 1.000.
public void test() {
// Map<String, Integer> map = new HashMap(16);
Map<String, Integer> map = new ConcurrentHashMap(16);
String key = "key";
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
incr(map, key);
// incrCompute(map, key);
}
countDownLatch.countDown();
}).start();
}
try {
//阻塞到线程跑完
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//1000不到
System.out.println(map.get(key));
}
private void incr(Map<String, Integer> map, String key) {
map.put(key, map.getOrDefault(key, 0) + 1);
}
No método de incremento automático incr, a operação de leitura é realizada primeiro, depois o cálculo é realizado e, por fim, a operação de escrita.Esta operação composta não garante atomicidade, portanto o acúmulo final de todos os resultados não deve ser 1000.
A maneira correta de usá-lo é usar o método padrão fornecido pelo JDK8compute
O princípio da implementação do ConcurrentHashMap compute
é usar meios de sincronização inseridos antes do cálculo.
private void incrCompute(Map<String, Integer> map, String key) {
map.compute(key, (k, v) -> Objects.isNull(v) ? 1 : v + 1);
}
estrutura de dados
Semelhante ao HashMap, implementado usando tabela hash + lista vinculada/árvore vermelho-preto
Tabela hash
A implementação da tabela hash é composta por arrays.Quando ocorre um conflito de hash (o algoritmo hash obtém o mesmo índice), o método de endereço de cadeia é usado para construir uma lista vinculada.
Quando os nós na lista vinculada são muito longos e a sobrecarga de travessia e pesquisa é alta e excede o limite (a lista vinculada tem mais de 8 nós e o comprimento da tabela hash é maior que 64), a árvore é transformada em um vermelho - árvore preta para reduzir a sobrecarga de travessia e pesquisa, e a complexidade do tempo é otimizada de O (n) é (log n)
ConcurrentHashMap é composto pelo array Node. Durante a expansão, haverá duas tabelas hash, antigas e novas: table e nextTable.
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
//哈希表 node数组
transient volatile Node<K,V>[] table;
//扩容时为了兼容读写,会存在两个哈希表,这个是新哈希表
private transient volatile Node<K,V>[] nextTable;
// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
//扩容时 用于指定迁移区间的下标
private transient volatile int transferIndex;
//统计每个哈希槽中的元素数量
private transient volatile CounterCell[] counterCells;
}
nó
O nó é usado para implementar os nós da matriz da tabela hash e os nós construídos em listas vinculadas quando ocorre um conflito de hash.
//实现哈希表的节点,数组和链表时使用
static class Node<K,V> implements Map.Entry<K,V> {
//节点哈希值
final int hash;
final K key;
volatile V val;
//作为链表时的 后续指针
volatile Node<K,V> next;
}
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
Valor de hash do nó
//转发节点
static final int MOVED = -1;
//红黑树在数组中的节点
static final int TREEBIN = -2;
//占位节点
static final int RESERVED = -3;
Nó de encaminhamento: Herde o nó e configure-o no primeiro nó de um índice na tabela hash antiga ao expandir a capacidade. Ao encontrar um nó de encaminhamento, você deve procurá-lo na nova tabela hash.
static final class ForwardingNode<K,V> extends Node<K,V> {
//新哈希表
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
//哈希值设置为-1
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
O nó TreeBin da árvore rubro-preta na matriz: herda o nó, primeiro aponta para o primeiro nó da árvore rubro-preta
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
//红黑树首节点
volatile TreeNode<K,V> first;
}
Nó de árvore vermelho-preto TreeNode
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
}
Nó de espaço reservado: Nó herdado. Quando o cálculo é necessário ( computer
método de uso), primeiro use o nó de espaço reservado para ocupar o lugar. Após o cálculo, o nó é construído para substituir o nó de espaço reservado.
static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null, null);
}
Node<K,V> find(int h, Object k) {
return null;
}
}
Princípio de implementação
estrutura
Durante a construção, os parâmetros de entrada são verificados, em seguida, a capacidade da tabela hash é calculada com base na capacidade de dados e no fator de carga a ser armazenado e, finalmente, a capacidade da tabela hash é ajustada à potência de 2.
Não é inicializado durante a construção, mas espera até ser usado antes de criá-lo (carregamento lento)
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
//检查负载因子、初始容量
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//concurrencyLevel:1
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
//计算大小 = 容量/负载因子 向上取整
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
//如果超过最大值就使用最大值
//tableSizeFor 将大小调整为2次幂
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
//设置容量
this.sizeCtl = cap;
}
ler-obter
O cenário de leitura usa volátil para garantir a visibilidade.Mesmo as modificações de outros threads são visíveis e não há necessidade de usar outros meios para garantir a sincronização.
A operação de leitura precisa encontrar elementos na tabela hash, embaralhar o valor hash por meio do algoritmo de perturbação e, em seguida, usar o valor hash para obter o índice por meio do algoritmo hash. Ele é dividido em múltiplas situações de acordo com o primeiro nó do índice .
-
O algoritmo de perturbação interrompe totalmente o valor do hash (para evitar causar muitos conflitos de hash) e o bit de sinal &0 garante que o resultado seja positivo.
int h = spread(key.hashCode())
Algoritmo de perturbação: operação XOR de 16 bits de valores de hash altos e baixos
Após o algoritmo de perturbação, &HASH_BITS = 0x7fffffff (011111...), o bit de sinal é 0 para garantir que o resultado seja um número positivo
Valores de hash negativos representam funções especiais, como nós de encaminhamento, primeiros nós de árvores, nós de espaço reservado, etc.
static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; }
-
Use o valor hash embaralhado para obter o índice (subscrito) na matriz por meio do algoritmo hash
n é o comprimento da tabela hash:
(n = tab.length)
(e = tabAt(tab, (n - 1) & h)
h é o valor hash calculado e a posição do índice pode ser encontrada pelo valor hash% (comprimento da tabela hash - 1)
Para melhorar o desempenho, é estipulado que o comprimento da tabela hash seja a n potência de 2. O comprimento da tabela hash em binário deve ser 1000...., e o
(n-1)
comprimento em binário deve ser 0111...Portanto,
(n - 1) & h
ao calcular o índice, o resultado da operação AND deve estar entre 0 e n - 1. Use operações de bits para melhorar o desempenho. -
Depois de obter os nós do array, você precisa compará-los
Depois de encontrar o primeiro nó na tabela hash, compare a chave para ver se é o nó atual.
Regras de comparação: compare os valores de hash primeiro. Se os valores de hash do objeto forem iguais, pode ser o mesmo objeto. Você também precisa comparar a chave (== e igual). Se os valores de hash forem não é o mesmo, então definitivamente não é o mesmo objeto.
A vantagem de comparar os valores de hash primeiro é melhorar o desempenho da pesquisa . Se igual for usado diretamente, a complexidade do tempo pode aumentar (como igual a String).
-
O método de endereço em cadeia é usado para resolver conflitos de hash, portanto, depois de encontrar o nó, você pode percorrer a lista ou árvore vinculada; devido à expansão da tabela de hash, você também pode ter que procurar um novo nó.
4.1 O primeiro nó é relativamente bem-sucedido e retorna diretamente
4.2 O valor hash do primeiro nó é negativo, indicando que o nó é um caso especial: nó de encaminhamento, primeiro nó da árvore, nó reservado de reserva calculada
- Se for um nó de encaminhamento e estiver sendo expandido, vá para o novo array para encontrá-lo.
- Se for TreeBin, pesquise na árvore rubro-preta
- Se for um nó de espaço reservado, retorne vazio diretamente.
4.3 Percorra a lista vinculada e compare sequencialmente
Obter código
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//1.spread:扰动算法 + 让key的哈希值不能为负数,因为负数哈希值代表红黑树或ForwardingNode
int h = spread(key.hashCode());
//2.(n - 1) & h:下标、索引 实际上就是数组长度模哈希值 位运算效率更高
//e:哈希表中对应索引位置上的节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
//3.如果哈希值相等,说明可能找到,再比较key
if ((eh = e.hash) == h) {
//4.1 key相等说明找到 返回
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//4.2 首节点哈希值为负,说明该节点是转发节点,当前正在扩容则去新数组上找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//4.3 遍历该链表,能找到就返回值,不能返回null
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
escrever
Ao adicionar elementos, use o método de sincronização CAS+sincronizado (bloqueando apenas um determinado primeiro nó na tabela hash) para garantir a atomicidade.
- Obtenha o valor do hash: algoritmo de perturbação + certifique-se de que o valor do hash seja positivo
- A tabela hash está vazia, o CAS garante uma inicialização do thread
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//小于0 说明其他线程在初始化 让出CPU时间片 后续初始化完退出
if ((sc = sizeCtl) < 0)
Thread.yield();
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
//CAS将SIZECTL设置成-1 (表示有线程在初始化)成功后 初始化
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
-
Passe o valor hash através do algoritmo hash para obter o nó no índice
f = tabAt(tab, i = (n - 1) & hash)
-
Manuseie de acordo com diferentes situações
-
4.1 Quando o primeiro nó está vazio, o CAS adiciona diretamente o nó à posição do índice.
casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))
-
4.2 Quando o hash do primeiro nó é MOVED -1, significa que o nó é um nó de encaminhamento, indicando que está se expandindo, ajudando a expandir a capacidade.
-
4.3 Bloqueio do primeiro nó
-
4.3.1 Percorra a lista vinculada para localizar e adicionar/substituir
-
4.3.2 Percorra a árvore para localizar e adicionar/sobrescrever
-
-
-
addCount
Conte os dados de cada nó e verifique a expansão
colocar código
//onlyIfAbsent为true时,如果原来有k,v则这次不会覆盖
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//1.获取哈希值:扰动算法+确保哈希值为正数
int hash = spread(key.hashCode());
int binCount = 0;
//乐观锁思想 CSA+失败重试
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//2.哈希表为空 CAS保证只有一个线程初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//3. 哈希算法求得索引找到索引上的首节点
//4.1 节点为空时,直接CAS构建节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//4.2 索引首节点hash 为MOVED 说明该节点是转发节点,当前正在扩容,去帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//4.3 首节点 加锁
synchronized (f) {
//首节点没变
if (tabAt(tab, i) == f) {
//首节点哈希值大于等于0 说明节点是链表上的节点
//4.3.1 遍历链表寻找然后添加/覆盖
if (fh >= 0) {
//记录链表上有几个节点
binCount = 1;
//遍历链表找到则替换,如果遍历完了还没找到就添加(尾插)
for (Node<K,V> e = f;; ++binCount) {
K ek;
//替换
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
//onlyIfAbsent为false允许覆盖(使用xxIfAbsent方法时,有值就不覆盖)
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;
}
}
}
//如果是红黑树首节点,则找到对应节点再覆盖
//4.3.2 遍历树寻找然后添加/覆盖
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//如果是添加返回null,返回不是null则出来添加
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
//覆盖
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//链表上的节点超过TREEIFY_THRESHOLD 8个(不算首节点) 并且 数组长度超过64才树化,否则扩容
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//5.添加计数,用于统计元素(添加节点的情况)
addCount(1L, binCount);
return null;
}
Expansão
Para evitar conflitos de hash frequentes, quando o número de elementos na tabela hash/o comprimento da tabela hash exceder o fator de carga, expanda a capacidade (aumente o comprimento da tabela hash)
De modo geral, a expansão consiste em aumentar o comprimento da tabela hash em 2 vezes. Por exemplo, de 32 a 64, o comprimento é garantido como uma potência de 2; se o comprimento da expansão atingir o limite superior do tipo inteiro, o o valor inteiro máximo é usado.
Quando ocorre a expansão, a lista vinculada ou árvore em cada slot do array precisa ser migrada para o novo array.
Se o processador for multi-core, essa operação de migração não será concluída apenas por um thread, mas outros threads também ajudarão na migração.
Durante a migração, deixe cada thread migrar vários slots da direita para a esquerda. Após a conclusão da migração, será avaliado se todas as migrações foram concluídas. Caso contrário, a migração continuará de maneira circular.
A operação de expansão ocorre principalmente no transfer
método, e a expansão ocorre principalmente em três cenários:
addCount
: Após adicionar o nó, aumente a contagem e verifique a expansão.helpTransfer
: Quando o thread é colocado, verifica-se que ele está migrando, para ajudar a ampliar a capacidade.tryPresize
: Tente ajustar a capacidade (adição em loteputAll
, chamada quando o comprimento da matriz da árvore não excede 64treeifyBin
)
Dividido nas 3 etapas a seguir
-
Calcule quantos slots migrar de cada vez com base no número de núcleos da CPU e no comprimento total da tabela hash, o mínimo é 16
-
A nova tabela hash está vazia, indicando que foi inicializada.
-
Migração circular
-
3.1 Aloque o intervalo [bround,i] responsável pela migração (pode haver migração simultânea de múltiplas threads)
-
3.2 Migração: dividida em migração de lista vinculada e migração de árvore
Migração de lista vinculada
-
Faça hash completo dos nós da lista vinculada no índice da nova tabela hash, índice + os dois subscritos do comprimento da tabela hash antiga (semelhante ao HashMap)
-
Coloque o nó na lista vinculada da posição do índice (hash e comprimento da tabela hash), o resultado é 0 na posição do índice da nova matriz, o resultado é 1 na posição do novo índice da matriz + o comprimento da antiga tabela hash
Por exemplo, o comprimento da tabela hash antiga é 16. No índice 3, o valor binário de 16 é 10.000, hash&16 => hash& 10.000. Ou seja, se o quinto bit do valor hash do nó for 0, é colocado na posição 3 da nova tabela hash. Se for 1, coloque-o no subscrito 3+16 da nova tabela hash.
-
Use o método de interpolação head para construir uma lista vinculada ln quando o resultado do cálculo for 0, e uma lista vinculada hn quando for 1. Para facilitar a construção da lista vinculada, o nó lastRun será encontrado primeiro: o nó lastRun e os nós subsequentes são todos nós da mesma lista vinculada, o que facilita a migração.
Construa lastRun primeiro antes de construir a lista vinculada, por exemplo, lastRun e->f na figura, primeiro coloque lastRun na lista vinculada ln, depois percorra a lista vinculada original, atravesse para a: a->e->f, atravesse para b: b->a ->e->f
-
Após a migração de cada posição do índice, o nó de encaminhamento é definido para a posição correspondente na tabela hash original.Quando outros threads realizam operações de leitura e obtenção, eles pesquisam na nova tabela hash de acordo com o nó de encaminhamento e realizam operações de gravação e colocação em ajudar a expandir a capacidade (migração de outros intervalos)
-
-
Código de expansão
//tab 旧哈希表
//nextTab 新哈希表
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
//1.计算每次迁移多少个槽
//n:哈希表长度(多少个槽)
int n = tab.length, stride;
//stride:每次负责迁移多少个槽
//NCPU: CPU核数
//如果是多核,每次迁移槽数 = 总槽数无符号右移3位(n/8)再除CPU核数
//每次最小迁移槽数 = MIN_TRANSFER_STRIDE = 16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//2.如果新哈希表为空,说明是初始化
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用于记录 每次负责迁移的槽右区间下标,从右往左分配,起始为最右
transferIndex = n;
}
//新哈希表长度
int nextn = nextTab.length;
//创建转发节点,转发节点一般设置在旧哈希表首节点,通过转发节点可以找到新哈希表
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//advance:是否继续循环迁移
boolean advance = true;
//
boolean finishing = false; // to ensure sweep before committing nextTab
//3.循环迁移
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//3.1 分配负责迁移的区间
//bound为左区间 i为右区间
while (advance) {
int nextIndex, nextBound;
//处理完一个槽 右区间 自减
if (--i >= bound || finishing)
advance = false;
//transferIndex<=0说明 要迁移的区间全分配完
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//CAS设置本次迁移的区间,防止多线程分到相同区间
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//3.2 迁移
//3.2.1 如果右区间i不再范围,说明迁移完
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果完成迁移,设置哈希表、数量
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//CAS 将sizeCtl数量-1 表示 一个线程迁移完成
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
//如果不是最后一条线程直接返回
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//是最后一条线程设置finishing为true 后面再循环 去设置哈希表、数量等操作
finishing = advance = true;
i = n; // recheck before commit
}
}
//3.2.2 如果旧哈希表i位置节点为空就CAS设置成转发节点
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//3.2.3 如果旧哈希表该位置首节点是转发节点,说明其他线程已处理,重新循环
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
//3.2.4 对首节点加锁 迁移
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
//3.2.4.1 链表迁移
//首节点哈希值大于等于0 说明 是链表节点
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
//寻找lastRun节点
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
//如果最后一次计算值是0
//lastRun节点以及后续节点计算值都是0构建成ln链表 否则 都是1构建成hn链表
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
//遍历构建ln、hn链表 (头插)
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
//头插:Node构造第四个参数是后继节点
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//设置ln链表到i位置
setTabAt(nextTab, i, ln);
//设置hn链表到i+n位置
setTabAt(nextTab, i + n, hn);
//设置转发节点
setTabAt(tab, i, fwd);
advance = true;
}
//3.2.4.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;
}
}
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;
}
}
}
}
}
}
O princípio de implementação não descreve muito sobre árvores rubro-negras. Por um lado, existem muitos conceitos de árvores rubro-negras e, por outro lado, quase os esqueci (já estou velho e posso ' não escrevo árvores rubro-negras à mão como na faculdade)
Outro aspecto é: acho que basta conhecer os benefícios do uso de árvores rubro-negras, e não é muito usado no trabalho, mesmo que a árvore rubro-negra mude de cor, gire para a esquerda ou para a direita para atender às condições do vermelho -árvore negra, não tem sentido., os alunos interessados podem simplesmente ir estudar.
Iterador
Iteradores em ConcurrentHashMap são fracamente consistentes e usam a tabela hash registrada para reconstruir novos objetos na busca
Iterador de entrada:
public Iterator<Map.Entry<K,V>> iterator() {
ConcurrentHashMap<K,V> m = map;
Node<K,V>[] t;
int f = (t = m.table) == null ? 0 : t.length;
return new EntryIterator<K,V>(t, f, 0, f, m);
}
iterador chave
public Enumeration<K> keys() {
Node<K,V>[] t;
int f = (t = table) == null ? 0 : t.length;
return new KeyIterator<K,V>(t, f, 0, f, this);
}
iterador de valor
public Enumeration<V> elements() {
Node<K,V>[] t;
int f = (t = table) == null ? 0 : t.length;
return new ValueIterator<K,V>(t, f, 0, f, this);
}
Resumir
ConcurrentHashMap usa a estrutura de dados de uma tabela hash. Quando ocorre um conflito de hash, ele usa o método de endereço de cadeia para resolvê-lo. Os nós com hash no mesmo índice são construídos em uma lista vinculada. Quando a quantidade de dados atinge um determinado limite, a lista vinculada é convertida em uma árvore rubro-negra.
ConcurrentHashMap usa modificação volátil para armazenar dados, tornando modificações em outros threads visíveis no cenário de leitura. Não há necessidade de usar um mecanismo de sincronização. CAS e synchronzied são usados para garantir a atomicidade no cenário de escrita.
Ao consultar dados com get, primeiro passe o valor hash da chave através do algoritmo de perturbação (XOR alto e baixo de 16 bits) e certifique-se de que o resultado seja um número positivo (com o bit de sinal superior 0) e, em seguida, combine-o com o comprimento da tabela hash superior -1 para encontrar o valor do índice, depois de encontrar o índice, pesquise de acordo com diferentes situações (a comparação determina primeiro o valor do hash e, em seguida, determina a chave se for igual)
Ao adicionar/sobrescrever dados em put, a posição do índice é primeiro encontrada através do algoritmo de perturbação e hash, e depois pesquisada de acordo com diferentes situações. Se encontrada, será sobrescrita, e se não for encontrada, será substituída.
Quando a expansão é necessária, o intervalo de slot que precisa ser migrado será organizado para o thread. Quando outros threads executam put, eles também ajudarão na migração. Cada vez que um thread migra um slot, o nó de encaminhamento será definido para o original tabela hash, para que haja consultas de thread. Você pode pesquisar a nova tabela hash através do nó de encaminhamento. Quando todos os slots forem migrados, deixe um thread para definir a tabela hash, quantidade, etc.
O iterador usa consistência fraca e um novo objeto é construído por meio de uma tabela hash ao obter o iterador.
ConcurrentHashMap garante apenas a segurança relativa do thread, mas não pode garantir a segurança absoluta do thread. Se você precisar realizar uma série de operações, deverá usá-lo corretamente.
Lei Jun: A versão oficial do novo sistema operacional da Xiaomi, ThePaper OS, foi empacotada. Uma janela pop-up na página da loteria Gome App insulta seu fundador. O governo dos EUA restringe a exportação da GPU NVIDIA H800 para a China. A interface do Xiaomi ThePaper OS é exposto. Um mestre usou Scratch para esfregar o simulador RISC-V e ele foi executado com sucesso. Kernel Linux RustDesk Remote Desktop 1.2.3 lançado, suporte aprimorado a Wayland Depois de desconectar o receptor USB da Logitech, o kernel Linux travou DHH revisão precisa de "ferramentas de empacotamento ": o front-end não precisa ser construído (Sem Build) JetBrains lança Writerside para criar documentação técnica Ferramentas para Node.js 21 lançadas oficialmenteEste artigo foi publicado pelo OpenWrite, um blog que publica vários artigos !