【源码】【Java并发】【ConcurrentHashMap】适合中学体质的ConcurrentHashMap

简介: 本文深入解析了ConcurrentHashMap的实现原理,涵盖JDK 7与JDK 8的区别、静态代码块、构造方法、put/get/remove核心方法等。JDK 8通过Node数组+链表/红黑树结构优化并发性能,采用CAS和synchronized实现高效锁机制。文章还详细讲解了hash计算、表初始化、扩容协助及计数更新等关键环节,帮助读者全面掌握ConcurrentHashMap的工作机制。

👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD

🔥 2025本人正在沉淀中... 博客更新速度++

👍 欢迎点赞、收藏、关注,跟上我的更新节奏

📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》

前言

经过上一篇的学习:
【Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门

你一定对ConcurrentHashMap有了一定的了解,忘记了也没关系,主播简答的带你回忆下,这篇要用的内容。
JDK7之前,是用Segment分段。那这样有什么问题呢?

  • 内存开销大 分段锁需预分配多个 Segment 对象(即使未使用),内存占用较高。
  • 并发粒度粗 分段锁的并发度由 Segment 数量固定(默认 16),无法动态扩展,高并发下仍可能竞争同一段。
  • 性能瓶颈 JDK 8 引入红黑树优化哈希冲突,分段锁难以适配这种复杂结构。

image.png

JDK 8 的改进

数据结构变化 JDK 8 的 ConcurrentHashMap 放弃了 Segment 分段锁,改用 Node 数组 + 链表/红黑树 结构,与 HashMap 类似。每个 Node 节点可独立加锁,锁粒度从“段级别”细化到“节点级别”。

锁机制升级 结合 CAS(Compare And Swap) 和 synchronized 关键字 实现并发控制:

CAS:用于无竞争场景(如初始化、计数器更新),避免加锁。

synchronized:仅在哈希冲突时对链表/树的头节点加锁,减少锁范围。

image.png

⚠以下源码是JDK1.8

静态代码块

比起构造方法,我们先来说说这个静态代码块。

通过预先计算字段偏移量和内存布局参数,使得 ConcurrentHashMap 在运行时能直接通过内存地址进行高效的无锁操作(如 CAS 更新 sizeCtl 或快速访问数组元素),从而支撑其高并发的核心逻辑(扩容、计数、节点操作)

static {
   
    try {
   
        // 获取 Unsafe 实例(用于底层内存操作)
        U = sun.misc.Unsafe.getUnsafe();

        // 获取 ConcurrentHashMap 类的 Class 对象
        Class<?> k = ConcurrentHashMap.class;

        // 计算关键字段在内存中的偏移量(用于后续 CAS 操作)
        SIZECTL = U.objectFieldOffset(k.getDeclDeclaredField("sizeCtl"));         // sizeCtl 控制表的初始化和扩容
        TRANSFERINDEX = U.objectFieldOffset(k.getDeclaredField("transferIndex")); // 扩容时的索引分配
        BASECOUNT = U.objectFieldOffset(k.getDeclaredField("baseCount"));          // 基础计数器(无竞争时使用)
        CELLSBUSY = U.objectFieldOffset(k.getDeclaredField("cellsBusy"));         // 计数器单元格的锁状态

        // 获取 CounterCell 内部类的 value 字段偏移量(用于分段计数)
        Class<?> ck = CounterCell.class;
        CELLVALUE = U.objectFieldOffset(ck.getDeclaredField("value"));

        // 计算 Node[] 数组的内存布局参数
        Class<?> ak = Node[].class;
        ABASE = U.arrayBaseOffset(ak);          // 数组首元素的内存基地址
        int scale = U.arrayIndexScale(ak);     // 数组中每个元素的占用字节数(如指针压缩后通常是 4)

        // 检查 scale 是否是 2 的幂次方(保证后续位运算有效)
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");

        // 计算 ASHIFT:用于快速定位数组元素的位移量(等价于 log2(scale))
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);  // 例如 scale=4 → ASHIFT=29
    } catch (Exception e) {
   
        throw new Error(e);  // 静态初始化失败直接抛 Error(类无法加载)
    }
}

构造方法

无参构造方法,这个没什么好说的了,啥也没干。

public ConcurrentHashMap() {
   
}

传了初始大小的方法:

public ConcurrentHashMap(int initialCapacity) {
   
    // 1. 参数校验:初始容量不能为负数
    if (initialCapacity < 0)
        throw new IllegalArgumentException();

    // 2. 计算实际初始容量(cap)
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY : // 若接近最大容量,直接取最大值(1 << 30)
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)); // 否则计算最小2的幂

    // 3. 将计算结果赋值给 sizeCtl(此时表示初始容量)
    this.sizeCtl = cap;
}

Put方法

image.png

首先是我们调用的put方法

public V put(K key, V value) {
   
    // key和value大家都好理解,第三个参数onlyIfAbsent的意思是要不要覆盖旧值
    return putVal(key, value, false);
}

putVal方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
   
    // ConcurrentHashMap的key和value都不能为null
    if (key == null || value == null) throw new NullPointerException();
    // 计算hash值,下面会具体说怎么来的
    int hash = spread(key.hashCode());
    int binCount = 0;
    // CAS添加
    for (Node<K,V>[] tab = table;;) {
   
        Node<K,V> f; int n, i, fh;
        // 如果还没初始化tab,就初始化(下面会具体说怎么初始化的)
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();    
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    // 定位桶并检查是否为空
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null))) // CAS 插入新节点
                break;                  
        }
        else if ((fh = f.hash) == MOVED) // 前节点的哈希值 f.hash 等于 MOVED 时,说明该节点是 ForwardingNode,表正在扩容。
            tab = helpTransfer(tab, f); // 让当前线程也帮助迁移表
        else {
   
            V oldVal = null;
            synchronized (f) {
    // 对当前桶的头节点 f 加锁(保证线程安全)
                if (tabAt(tab, i) == f) {
      // 再次检查当前桶的头节点是否还是 f(防止其他线程修改)
                    if (fh >= 0) {
     // 如果头节点的哈希值 fh >= 0,说明是链表结构
                        binCount = 1; // // 初始化链表节点计数器
                        // 遍历这个链表
                        for (Node<K,V> e = f;; ++binCount) {
   
                            K ek;
                            // 判断链表里,是不是已经有个这Node了
                            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;
                            // (e = e.next) == null说明,这个链表没有相同的Node,那就添加这个Node
                            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; // 红黑树节点的 binCount 固定为 2(简化统计)
                        // 调用红黑树的插入方法 putTreeVal
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
   
                            oldVal = p.val;
                            if (!onlyIfAbsent) // 如果允许覆盖,更新值
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
    // 如果 binCount 非零,说明进行了插入或更新操作
                if (binCount >= TREEIFY_THRESHOLD) // 如果链表长度达到树化阈值(默认8)
                    treeifyBin(tab, i); // 尝试将链表转为红黑树(或扩容)
                if (oldVal != null) // 如果存在旧值,返回旧值(不覆盖或更新时)
                    return oldVal;
                break;
            }
        }
    }
    /// 做了2件事,1、维护线程安全计数 2、触发扩容检查
    addCount(1L, binCount);
    return null;
}

// 树化阈值
static final int TREEIFY_THRESHOLD = 8;

spread-计算hash值

在我们putVal的时候,会有槽位的计算

final V putVal(K key, V value, boolean onlyIfAbsent) {
   
    if (key == null || value == null) throw new NullPointerException();
    // hash桶位
    int hash = spread(key.hashCode());
...

聪明的你,是否会好奇,这个槽位是怎么算出来的,没有关系,跟上主播的节奏,我们先来看看spread方:

static final int spread(int h) {
   
    return (h ^ (h >>> 16)) & HASH_BITS;
}

static final int HASH_BITS = 0x7fffffff; // 正常节点的哈希可用位

原始哈希码的问题

假设你的键(Key)的hashCode()返回了一个整数h,比如 h = 0x12345678(32位二进制数)。\
当哈希表的大小(桶数量)较小时(比如n=16),计算桶位置的公式是:\
index = (n-1) & hash,即 15 & hash(因为n=16n-1=15,二进制是0000...1111)。\
此时只有哈希值的低4位参与计算,高位完全被忽略。如果多个键的哈希值低位相同但高位不同,它们会被分配到同一个桶,导致哈希冲突。

1的哈希值:0xABCD1111 → 低4位是0001 → 桶位置12的哈希值:0x12341111 → 低4位是0001 → 桶位置1
虽然高位完全不同,但低4位相同 → 冲突!

扰动函数:h ^ (h >>> 16)

ConcurrentHashMap通过扰动函数将高位信息传播到低位,解决上述问题。

步骤拆解

  1. 右移16位h >>> 16\
    将高16位移到低16位,高16位补0。\
    例如,h = 0x12345678h >>> 16 = 0x00001234
  2. 异或操作h ^ (h >>> 16)\
    将原哈希值的高16位和低16位混合。
h          = 0001 0010 0011 0100 0101 0110 0111 1000 (0x12345678)
h >>> 16   = 0000 0000 0000 0000 0001 0010 0011 0100 (0x00001234)
h ^ (h>>>16) = 0001 0010 0011 0100 0100 0100 0100 1100 (0x1234444C)

扰动后,它们的低位不同 → 桶位置不同,冲突被解决!

屏蔽符号位:& HASH_BITS

HASH_BITS = 0x7FFFFFFF(二进制最高位是0,其余位是1)。\
这一步的目的是将哈希值的最高位强制设为0,确保结果为非负数。

那聪明的你一定会问,为什么要这样做?

  • 特殊节点的标记:ConcurrentHashMap内部用负数哈希值标记特殊节点(如树节点、转发节点)。

    • 例如,树节点的哈希值是-2(即0x80000001)。
  • 避免冲突:如果普通键的哈希值是负数(如0x80000001),会与树节点的哈希值冲突。\
    通过& HASH_BITS,所有普通键的哈希值都变为非负数,与特殊节点明确区分。

原哈希值:h = 0x80001234(最高位是1,负数)
h & 0x7FFFFFFF → 0x00001234(最高位变为0,正数)

initTable-初始化tab

我们在putVal的时候,如果tab为null或者没有大小,就初始化

private final Node<K,V>[] initTable() {
   
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
   
        // sizeCtl 是一个 volatile 变量,用于控制表的初始化和扩容。
        // 当 sizeCtl < 0 时,表示其他线程正在初始化或扩容(此时当前线程需等待)。
        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;
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2); // 计算扩容阈值:n - n/4 = 0.75n
                }
            } finally {
   
                sizeCtl = sc;    // 恢复 sizeCtl 为扩容阈值(正数)
            }
            break;
        }
    }
    return tab;
}

// 默认大小16
private static final int DEFAULT_CAPACITY = 16;

helpTransfer-帮助一起扩容

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
   
    Node<K,V>[] nextTab; int sc;
    // 1、tab 必须非空,表示当前哈希表有效。
    // 2、f 必须是 ForwardingNode(哈希值为 MOVED 的占位节点)。
    // 3、ForwardingNode 的 nextTable 字段必须非空(新表已初始化)。
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
   
        // 根据旧表长度生成一个唯一的扩容标识(rs)
        int rs = resizeStamp(tab.length);
        // 循环检查扩容状态:这些条件确保当前扩容尚未完成,且扩容上下文未发生变化
        // 1、nextTab 必须仍是当前扩容的新表(未被其他线程修改)
        // 2、table 必须仍是旧表(未被其他线程更新
        // 3、sizeCtl 必须为负数(表示扩容正在进行)
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
   
            // 扩容终止条件检查,满足任一条件时,退出循环,不再参与扩容
            // 1、(sc >>> RESIZE_STAMP_SHIFT) != rs:扩容标识不匹配,说明当前扩容已结束或属于其他扩容阶段。
            // 2、sc == rs + 1 或 sc == rs + MAX_RESIZERS:扩容线程数已达上限(MAX_RESIZERS 为最大允许线程数)
            // 3、transferIndex <= 0:所有桶已分配完毕,无需更多线程参与迁移
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            // CAS 竞争扩容线程名额
            // 通过原子操作将 sizeCtl 从 sc 增加到 sc + 1,表示新增一个线程参与扩容
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
   
                // 具体扩容逻辑
                transfer(tab, nextTab);     
                break;
            }
        }
        return nextTab;        // 返回新表
    }
    return table;    // 返回旧表
}

// 生成一个扩容标识
// 高位:旧表长度的二进制特征(用于区分不同扩容阶段)
// 低位:固定掩码(保证结果为正数,与 sizeCtl 的负数状态兼容)
static final int resizeStamp(int n) {
   
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
private static int RESIZE_STAMP_BITS = 16;

addCount-更新元素总数,并在必要时触发扩

// 入参数解析
// x      : 要增加的元素数量(通常是1,插入时增加;-1,删除时减少)
// check  : 是否需要检查扩容(插入操作通常传递 binCount 的值,删除操作可能传递0)
private final void addCount(long x, int check) {
   
    CounterCell[] as; long b, s;
    // 1. 判断是否使用分片计数(CounterCell 数组是否已初始化)
    if ((as = counterCells) != null || 
        // 2. 尝试直接更新 baseCount(无竞争时快速路径)
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
   
        // 进入分片计数逻辑(存在竞争时)
        CounterCell a; long v; int m;
        boolean uncontended = true;
        // 检查分片计数是否可用:
        // - CounterCell 数组未初始化?
        // - 当前线程的哈希槽位是否未分配?
        // - 尝试更新槽位的值是否失败?
        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;
        }
        // 如果 check <= 1,不需要触发扩容检查(例如删除操作或链表未超长)
        if (check <= 1)
            return;
        // 计算当前总元素数(baseCount + 所有 CounterCell 的值)
        s = sumCount();
    }
}

Get方法

主播的评价是,比put的方法,看起来更友善一些( ̄▽ ̄)"

image.png

public V get(Object key) {
   
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 计算hash值,上面有具体说过这个方法了
    int h = spread(key.hashCode());
    // 判断table是否已经初始化了 且 当前槽位有Node
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
   
        // 直接匹配头节点,如果key相同的话,直接就返回了
        if ((eh = e.hash) == h) {
   
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 处理特殊节点(树或转移节点)
        // 若头节点哈希值为负,表示当前桶可能为红黑树(树节点)或正在扩容(转移节点)
        // 调用find方法在树或链表中搜索
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 遍历链表,找元素
        while ((e = e.next) != null) {
   
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    // 都没有找到的话,直接返回null
    return null;
}

Remove方法

remove方法,其实和put方法差不多

image.png

public V remove(Object key) {
   
    // replaceNode的三个参数的意思
    // key: 要操作的键。
    // value: 新值(若为 null 表示删除操作)。
    // cv (Condition Value): 条件值(只有原值等于 cv 时才允许操作)。
    return replaceNode(key, null, null);
}

具体的replaceNode方法:

final V replaceNode(Object key, V value, Object cv) {
   
    // 计算hash值
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
   
        Node<K,V> f; int n, i, fh;
        // table没有初始化 或者这个桶位的头Node都找不到,就返回退出
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        else if ((fh = f.hash) == MOVED) // 判断是否需要协助一起迁移表
            tab = helpTransfer(tab, f);
        else {
   
            V oldVal = null;
            boolean validated = false; // validated 标记是否成功处理了链表或树结构
            // 锁定当前桶的头节点 f
            synchronized (f) {
   
                // 再次确认头节点未被其他线程修改
                if (tabAt(tab, i) == f) {
    
                    if (fh >= 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)))) {
    // 键匹配
                                V ev = e.val;
                                // 检查条件值 cv(若 cv 为 null,则无条件操作)
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
   
                                    oldVal = ev;  // 记录旧值
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;        // 删除这个节点
                                    else
                                        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) {
     // 查找树节点
                            V pv = p.val; 
                            // 检查条件值 cv(若 cv 为 null,则无条件操作)
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
   
                                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)
                        addCount(-1L, -1); // 更新计数(删除操作)
                    return oldVal;    // 返回旧值
                }
                break;
            }
        }
    }
    return null;
}

后话

看到这里的你,一定对ConcurrentHashMap有了更深刻的了解了吧!

小手手点波关注,主播马上要开新篇了😘

跟上主播的节奏!!!

目录
相关文章
|
24天前
|
前端开发 Java 关系型数据库
基于Java+Springboot+Vue开发的鲜花商城管理系统源码+运行
基于Java+Springboot+Vue开发的鲜花商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的鲜花商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。技术学习共同进步
113 7
|
1月前
|
消息中间件 算法 安全
JUC并发—1.Java集合包底层源码剖析
本文主要对JDK中的集合包源码进行了剖析。
|
1月前
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
72 5
|
6天前
|
JavaScript Java 关系型数据库
家政系统源码,java版本
这是一款基于SpringBoot后端框架、MySQL数据库及Uniapp移动端开发的家政预约上门服务系统。
家政系统源码,java版本
|
17天前
|
供应链 JavaScript 前端开发
Java基于SaaS模式多租户ERP系统源码
ERP,全称 Enterprise Resource Planning 即企业资源计划。是一种集成化的管理软件系统,它通过信息技术手段,将企业的各个业务流程和资源管理进行整合,以提高企业的运营效率和管理水平,它是一种先进的企业管理理念和信息化管理系统。 适用于小微企业的 SaaS模式多租户ERP管理系统, 采用最新的技术栈开发, 让企业简单上云。专注于小微企业的应用需求,如企业基本的进销存、询价,报价, 采购、销售、MRP生产制造、品质管理、仓库库存管理、财务应收付款, OA办公单据、CRM等。
99 23
|
1月前
|
缓存 安全 Java
【Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门
ConcurrentHashMap是Java中线程安全的哈希表实现,支持高并发读写操作。相比Hashtable,它通过分段锁(JDK1.7)或CAS+synchronized(JDK1.8)实现更细粒度锁控制,提升性能与安全性。本文详细介绍其构造方法、添加/获取/删除元素等常用操作,并对比JDK1.7和1.8的区别,帮助开发者深入理解与使用ConcurrentHashMap。欢迎关注,了解更多!
72 4
【Java并发】【ConcurrentHashMap】适合初学体质的ConcurrentHashMap入门
|
1月前
|
Java
【源码】【Java并发】【LinkedBlockingQueue】适合中学体质的LinkedBlockingQueue入门
前言 有了前文对简单实用的学习 【Java并发】【LinkedBlockingQueue】适合初学体质的LinkedBlockingQueue入门 聪明的你,一定会想知道更多。哈哈哈哈哈,下面主播就...
51 6
【源码】【Java并发】【LinkedBlockingQueue】适合中学体质的LinkedBlockingQueue入门
|
1月前
|
安全 Java
【源码】【Java并发】【ArrayBlockingQueue】适合中学者体质的ArrayBlockingQueue
前言 通过之前的学习是不是学的不过瘾,没关系,马上和主播来挑战源码的阅读 【Java并发】【ArrayBlockingQueue】适合初学体质的ArrayBlockingQueue入门 还有一件事
52 5
【源码】【Java并发】【ArrayBlockingQueue】适合中学者体质的ArrayBlockingQueue
|
29天前
|
Java 关系型数据库 MySQL
Java汽车租赁系统源码(含数据库脚本)
Java汽车租赁系统源码(含数据库脚本)
44 4
|
存储 Java
Java集合源码解析-ConcurrentHashMap(JDK8)(下)
Java集合源码解析-ConcurrentHashMap(JDK8)
163 0
Java集合源码解析-ConcurrentHashMap(JDK8)(下)