ConcurrentHashMap1-8源码解读及如何保证线程安全,2024年最新万字解析

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注网络安全)
img

正文

}

TreeNode<K,V> xp = p; // 定义xp指向当前节点
/*

  • 如果dir小于等于0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较
  • 如果dir大于等于0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较
  • 如果以上两条当中有一个子节点不为空,这个if中还做了一件事,那就是把p已经指向了对应的不为空的子节点,开始下一轮的比较
    */
    if ((p = (dir <= 0) ? p.left : p.right) == null) {
    // 如果恰好要添加的方向上的子节点为空,此时节点p已经指向了这个空的子节点
    TreeNode<K,V> x, f = first; // 获取当前节点的next节点
    first = x = new TreeNode<K,V>(h, k, v, f, xp);// 创建一个新的树节点
    if (f != null)
    f.prev = x;
    if (dir <= 0)
    xp.left = x; // 左孩子指向到这个新的树节点
    else
    xp.right = x;// 右孩子指向到这个新的树节点
    if (!xp.red)
    x.red = true;
    else {
    lockRoot();//加锁
    try {
    root = balanceInsertion(root, x);// 重新平衡,以及新的根节点置顶
    } finally {
    unlockRoot();//解锁
    }
    }
    break;
    }
    }
    assert checkInvariants(root);
    return null;
    }

此函数用于将指定的hash、key、value值添加到红黑树中,若已经添加了,则返回null,否则返回该结点。

treeifyBin函数

当数组长度小于64的时候,扩张数组长度一倍,否则的话把链表转为树

private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
System.out.println(“treeifyBin方\t==>数组长:”+tab.length);
if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //MIN_TREEIFY_CAPACITY 64
tryPresize(n << 1); // 数组扩容
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) { //使用synchronized同步器,将该节点出的链表转为树
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null; //hd:树的头(head)
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) //把Node组成的链表,转化为TreeNode的链表,头结点任然放在相同的位置
hd = p; //设置head
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));//把TreeNode的链表放入容器TreeBin中
}
}
}
}
}

此函数用于将桶中的数据结构转化为红黑树,其中,值得注意的是,当table的长度未达到阈值时,会进行一次扩容操作,该操作会使得触发treeifyBin操作的某个桶中的所有元素进行一

次重新分配,这样可以避免某个桶中的结点数量太大。

tryPresize方法

private final void tryPresize(int size) {
/*

  • MAXIMUM_CAPACITY = 1 << 30
  • 如果给定的大小大于等于数组容量的一半,则直接使用最大容量,
  • 否则使用tableSizeFor算出来
  • 后面table一直要扩容到这个值小于等于sizeCtrl(数组长度的3/4)才退出扩容
    /
    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;
    /
  • 如果数组table还没有被初始化,则初始化一个大小为sizeCtrl和刚刚算出来的c中较大的一个大小的数组
  • 初始化的时候,设置sizeCtrl为-1,初始化完成之后把sizeCtrl设置为数组长度的3/4
  • 为什么要在扩张的地方来初始化数组呢?
  • 这是因为如果第一次put的时候不是put单个元素,
  • 而是调用putAll方法直接put一个map的话,在putALl方法中没有调用initTable方法去初始化table,
  • 而是直接调用了tryPresize方法,所以这里需要做一个是不是需要初始化table的判断
    /
    if (tab == null || (n = tab.length) == 0) {
    n = (sc > c) ? sc : c;
    if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //初始化tab的时候,把sizeCtl设为-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;
    }
    }
    }
    /
  • 一直扩容到的c小于等于sizeCtl或者数组长度大于最大长度的时候,则退出
  • 所以在一次扩容之后,不是原来长度的两倍,而是2的n次方倍
    /
    else if (c <= sc || n >= MAXIMUM_CAPACITY) {
    break; //退出扩张
    }
    else if (tab == table) {
    int rs = resizeStamp(n);
    /
  • 如果正在扩容Table的话,则帮助扩容
  • 否则的话,开始新的扩容
  • 在transfer操作,将第一个参数的table中的元素,移动到第二个元素的table中去,
  • 虽然此时第二个参数设置的是null,但是,在transfer方法中,当第二个参数为null的时候,
  • 会创建一个两倍大小的table
    /
    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;
    /
  • transfer的线程数加一,该线程将进行transfer的帮忙
  • 在transfer的时候,sc表示在transfer工作的线程数
    /
    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);
    }
    }
    }
    }

在tryPresize方法中,并没有加锁,允许多个线程进入,如果数组正在扩张,则当前线程也去帮助扩容。

addCount方法

rivate final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { // counterCells不为空或者比较交换失败
CounterCell a; long v; int m;
// 无竞争标识
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // cas
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
//是否需要进行扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
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);
s = sumCount();
}
}
}

此函数主要完成binCount的值加1的操作。

扩容方法

jdk1.8transfer方法

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) //MIN_TRANSFER_STRIDE 用来控制不要占用太多CPU
stride = MIN_TRANSFER_STRIDE; // subdivide range //MIN_TRANSFER_STRIDE=16
/*

  • 如果复制的目标nextTab为null的话,则初始化一个table两倍长的nextTab
  • 此时nextTable被设置值了(在初始情况下是为null的)
  • 因为如果有一个线程开始了表的扩张的时候,其他线程也会进来帮忙扩张,
  • 而只是第一个开始扩张的线程需要初始化下目标数组
    /
    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;
    /
  • 创建一个fwd节点,这个是用来控制并发的,当一个节点为空或已经被转移之后,就设置为fwd节点
  • 这是一个空的标志节点
    /
    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); //设置sizeCtl为扩容后的0.75
    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) //数组中把null的元素设置为ForwardingNode节点(hash值为MOVED[-1])
    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) { //该节点的hash值大于等于0,说明是一个Node节点
    /
  • 因为n的值为数组的长度,且是power(2,x)的,所以,在&操作的结果只可能是0或者n
  • 根据这个规则
  •     0-->  放在新表的相同位置
    
  •     n-->  放在新表的(n+原来位置)
    

/
int runBit = fh & n;
Node<K,V> lastRun = f;
/

  • lastRun 表示的是需要复制的最后一个节点
  • 每当新节点的hash&n -> b 发生变化的时候,就把runBit设置为这个结果b
  • 这样for循环之后,runBit的值就是最后不变的hash&n的值
  • 而lastRun的值就是最后一次导致hash&n 发生变化的节点(假设为p节点)
  • 为什么要这么做呢?因为p节点后面的节点的hash&n 值跟p节点是一样的,
  • 所以在复制到新的table的时候,它肯定还是跟p节点在同一个位置
  • 在复制完p节点之后,p节点的next节点还是指向它原来的节点,就不需要进行复制了,自己就被带过去了
  • 这也就导致了一个问题就是复制后的链表的顺序并不一定是原来的倒序
    /
    for (Node<K,V> p = f.next; p != null; p = p.next) {
    int b = p.hash & n; //n的值为扩张前的数组的长度
    if (b != runBit) {
    runBit = b;
    lastRun = p;
    }
    }
    if (runBit == 0) {
    ln = lastRun;
    hn = null;
    }
    else {
    hn = lastRun;
    ln = null;
    }
    /
  • 构造两个链表,顺序大部分和原来是反的
  • 分别放到原来的位置和新增加的长度的相同位置(i/n+i)
    /
    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)
    /
  • 假设runBit的值为0,
  • 则第一次进入这个设置的时候相当于把旧的序列的最后一次发生hash变化的节点(该节点后面可能还有hash计算后同为0的节点)设置到旧的table的第一个hash计算后为0的节点下一个节点
  • 并且把自己返回,然后在下次进来的时候把它自己设置为后面节点的下一个节点
    /
    ln = new Node<K,V>(ph, pk, pv, ln);
    else
    /
  • 假设runBit的值不为0,
  • 则第一次进入这个设置的时候相当于把旧的序列的最后一次发生hash变化的节点(该节点后面可能还有hash计算后同不为0的节点)设置到旧的table的第一个hash计算后不为0的节点下一个节点
  • 并且把自己返回,然后在下次进来的时候把它自己设置为后面节点的下一个节点
    /
    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;
    }
    }
    /
  • 在复制完树节点之后,判断该节点处构成的树还有几个节点,
  • 如果≤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;
    }
    }
    }
    }
    }
    }

特点

1.把数组中的节点复制到新的数组的相同位置,或者移动到扩张部分的相同位置

2.在这里首先会计算一个步长,表示一个线程处理的数组长度,用来控制对CPU的使用,

3.每个CPU最少处理16个长度的数组元素,也就是说,如果一个数组的长度只有16,那只有一个线程会对其进行扩容的复制移动操作

4.扩容的时候会一直遍历,直到复制完所有节点,每处理一个节点的时候会在链表的头部设置一个fwd节点,这样其他线程就会跳过他,

5.复制后在新数组中的链表不是绝对的反序的

get方法

get方法很简单,支持并发操作,也不加锁

1.当key为null的时候回抛出NullPointerException的异常

2.get操作通过首先计算key的hash值来确定该元素放在数组的哪个位置

3.然后遍历该位置的所有节点

4.如果存在返回值,如果不存在的话返回null

public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算key的hash值
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { // 表不为空并且表的长度大于0并且key所在的桶不为空
if ((eh = e.hash) == h) { // 表中的元素的hash值与key的hash值相等
if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 键相等
// 返回值
return e.val;
}
else if (eh < 0) // 结点hash值小于0
// 在桶(链表/红黑树)中查找
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { // 对于结点hash值大于0的情况
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}

size方法

size源码

public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}

最大值是 Integer 类型的最大值,但是 Map 的 size 可能超过 MAX_VALUE, 所以还有一个方法 mappingCount(),JDK 的建议使用 mappingCount() 而不是size()。

mappingCount() 的代码如下:

public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}

以上可以看出,无论是 size() 还是 mappingCount(), 计算大小的核心方法都是 sumCount()。

sumCount() 的代码如下:

final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}

分析一下 sumCount() 代码。ConcurrentHashMap 提供了 baseCount、counterCells 两个辅助变量和一个 CounterCell 辅助内部类。

sumCount() 就是迭代 counterCells 来统计 sum 的过程。

put 操作时,肯定会影响 size(),在 put() 方法最后会调用 addCount() 方法。

addCount() 中有如下代码:

如果 counterCells == null, 则对 baseCount 做 CAS 自增操作。

if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;

如果并发导致 baseCount CAS 失败了使用 counterCells。

if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();

如果counterCells CAS 失败了,在 fullAddCount 方法中,会继续死循环操作,直到成功。

private final void fullAddCount(long x, boolean wasUncontended) {
int h;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
ThreadLocalRandom.localInit(); // force initialization
h = ThreadLocalRandom.getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
for (;😉 {//死循環,直到成功
CounterCell[] as; CounterCell a; int n; long v;
if ((as = counterCells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to atta

然后,CounterCell 这个类到底是什么?我们会发现它使用了 @sun.misc.Contended 标记的类,内部包含一个 volatile 变量。

@sun.misc.Contended 这个注解标识着这个类防止需要防止 “伪共享”。那么,

什么又是伪共享呢?

缓存系统中是以缓存行(cache line)为单位存储的。

缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。

当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。 CounterCell 代码如下:

@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}

JDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size。

JDK 8 推荐使用mappingCount 方法,因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值。

remove方法

先調用remove方法

在方法內調用replaceNode方法

public V remove(Object key) {
return replaceNode(key, null, null);
}

replaceNode方法

final V replaceNode(Object key, V value, Object cv) {
// 计算key的hash值
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;😉 { // 无限循环
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null) // table表为空或者表长度为0或者key所对应的桶为空
// 跳出循环
break;
else if ((fh = f.hash) == MOVED) // 桶中第一个结点的hash值为MOVED
// 转移
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
synchronized (f) { // 加锁同步
if (tabAt(tab, i) == f) { // 桶中的第一个结点没有发生变化
if (fh >= 0) { // 结点hash值大于0
validated = true;
for (Node<K,V> e = f, pred = null;😉 { // 无限循环
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { // 结点的hash值与指定的hash值相等,并且key也相等
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) { // cv为空或者与结点value相等或者不为空并且相等
// 保存该结点的val值
oldVal = ev;
if (value != null) // value为null
// 设置结点value值
e.val = value;
else if (pred != null) // 前驱不为空
// 前驱的后继为e的后继,即删除了e结点
pred.next = e.next;
else
// 设置table表中下标为index的值为e.next
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
else if (f instanceof TreeBin) { // 为红黑树结点类型
validated = true;
// 类型转化
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) { // 根节点不为空并且存在与指定hash和key相等的结点
// 保存p结点的value
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) { // cv为空或者与结点value相等或者不为空并且相等
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode§) // 移除p结点
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
// baseCount值减一
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}

clear 方法

public void clear() {
long delta = 0L; // negative number of deletions
int i = 0;
Node<K,V>[] tab = table;
//循环table,依次情况
while (tab != null && i < tab.length) {

int fh;
Node<K,V> f = tabAt(tab, i);
if (f == null)
++i;
else if ((fh = f.hash) == MOVED) {
//如果存在转移的,先进行转移,再删除
tab = helpTransfer(tab, f);
i = 0; // restart
}
else {
//加锁删除
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> p = (fh >= 0 ? f :
(f instanceof TreeBin) ?
((TreeBin<K,V>)f).first : null);
while (p != null) {
–delta;
p = p.next;
}
setTabAt(tab, i++, null);
}
}
}
}
if (delta != 0L)
addCount(delta, -1);
}

线程安全的原因

jdk1.7

分段锁的原因,可重入锁

Jdk1.8

ConcurrentHashMap的实现使用的是锁分离思想,只是锁住的是一个node,而锁住Node之前的操作是基于在volatile和CAS之上无锁并且线程安全的。

多线程下如何保证线程安全

初始化的线程安全保障

table变量使用了volatile来保证每次获取到的都是最新写入的值

transient volatile Node<K,V>[] table;

put方法的多线程安全保障

多个线程同时进行put操作,在初始化数组时使用了乐观锁CAS操作来决定到底是哪个线程有资格进行初始化,其他线程均只能等待。

用到的并发技巧:

1.volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。

2.CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功

在tabAt(tab, i)方法,其使用Unsafe类volatile的操作volatile式地查看值,保证每次获取到的值都是最新的:

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

虽然上面的table变量加了volatile,但也只能保证其引用的可见性,并不能确保其数组中的对象是否是最新的,所以需要Unsafe类volatile式地拿到最新的Node

扩容的线程安全性

并发技巧:

1.减小锁粒度:将Node链表的头节点作为锁,若在默认大小16情况下,将有16把锁,大大减小了锁竞争(上下文切换),就像开头所说,将串行的部分最大化缩小,在理想情况下线程的put操作都

为并行操作。同时直接锁住头节点,保证了线程安全

2.Unsafe的getObjectVolatile方法:此方法确保获取到的值为最新

ConcurrentHashMap运用各类CAS操作,将扩容操作的并发性能实现最大化,在扩容过程中,

就算有线程调用get查询方法,也可以安全的查询数据,若有线程进行put操作,还会协助扩容,

利用sizeCtl标记位和各种volatile变量进行CAS操作达到多线程之间的通信、协助,在迁移过程中只锁一个Node节点,即保证了线程安全,又提高了并发性能。

多个线程又是如何同步处理的呢?

在ConcurrentHashMap中,同步处理主要是通过Synchronized和unsafe两种方式来完成的。

1.在取得sizeCtl、某个位置的Node的时候,使用的都是unsafe的方法,来达到并发安全的目的

2.当需要在某个位置设置节点的时候,则会通过Synchronized的同步机制来锁定该位置的节点。

3.在数组扩容的时候,则通过处理的步长和fwd节点来达到并发安全的目的,通过设置hash值为MOVED

4.当把某个位置的节点复制到扩张后的table的时候,也通过Synchronized的同步机制来保证现程安全

快速失败(fail-fast)和安全失败(fail-safe)的区别

fail-fast和fail-safe比较

Iterator的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。

java.util包下面的所有的集合类都是快速失败的,而java.util.concurrent包下面的所有的类都是安全失败的。

快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败的迭代器永远不会抛出这样的异常。

由于ConcurrentHashMap也是java.util.concurrent下的,所以它是安全失败(fail-safe)的。

如何解决

fail-fast机制,是一种错误检测机制。

它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。

若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。

fail-fast原理

产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的。

比如java.util下的集合在调用 next() 和 remove()时,

都会执行 checkForComodification()。

若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。

总结fail-fast是如何产生的

当多个线程对同一个集合进行操作的时候,某线程访问集合的过程中,

该集合的内容被其他线程所改变(即其它线程通过add、remove、clear等方法,改变了modCount的值);

这时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

解决fail-fast的原理

比如在CopyOnWriteArrayList的源码中

迭代时自己实现迭代器

// 返回集合对应的迭代器
public Iterator iterator() {
return new COWIterator(getArray(), 0);
}

private static class COWIterator implements ListIterator {
private final Object[] snapshot;

private int cursor;

private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
// 新建COWIterator时,将集合中的元素保存到一个新的拷贝数组中。
// 这样,当原始集合的数据改变,拷贝数据中的值也不会变化。
snapshot = elements;
}

其中,是会将集合中的元素拷贝到一个新的数组中,

当原始集合的数据改变,拷贝数据中的值也不会变化。

所以不存在fail-fast问题。

CopyOnWriteArrayList的Iterator实现类中,没有所谓的checkForComodification(),更不会抛出ConcurrentModificationException异常。

ConcurrentHashMap、HashMap、HashTable的比较

HashMap 和 ConcurrentHashMap 的区别

1.ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,

相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)

HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

ConcurrentHashMap 和 Hashtable 的区别?

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

Hashtable是整个加锁。

ConcurrentHashMap的1.7是分段锁,1.8是cas加synchronized实现线程安全,粒度都更细。

写在最后

在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。

需要完整版PDF学习资源私我

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

在每一个分段上都用lock锁进行保护,

相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)

HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。

ConcurrentHashMap 和 Hashtable 的区别?

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

Hashtable是整个加锁。

ConcurrentHashMap的1.7是分段锁,1.8是cas加synchronized实现线程安全,粒度都更细。

写在最后

在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。

需要完整版PDF学习资源私我

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注网络安全)
[外链图片转存中…(img-xXXIUkbB-1713122227387)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值