文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

如何理解Java 容器中并发容器的源码分析

2023-06-05 04:23

关注

这期内容当中小编将会给大家带来有关如何理解Java 容器中并发容器的源码分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

如果没有特别说明,以下源码分析基于 JDK 1.8。

CopyOnWriteArrayList

1.读写分离

写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

public Boolean add(E e) {    //加锁    final ReentrantLock lock = this.lock;    lock.lock();    try {        Object[] elements = getArray();        int len = elements.length;        // newElements 是一个复制的数组        Object[] newElements = Arrays.copyOf(elements, len + 1);        newElements[len] = e;        // 写操作在一个复制的数组上进行        setArray(newElements);        return true;    }    finally {        lock.unlock();    }}final void setArray(Object[] a) {    array = a;}
@SuppressWarnings("unchecked")private E get(Object[] a, int index) {    //读取操作仍然在原始的数组中    return (E) a[index];}

2.适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,很适合读多写少的应用场景。

CopyOnWriteArrayList 有其缺陷:

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

二、ConcurrentHashMap

1. 存储结构

static final class HashEntry<K,V> {    final int hash;    final K key;    volatile V value;    volatile HashEntry<K,V> next;}

ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶, 从而使其并发度更高(并发度就是 Segment 的个数)。

Segment 继承自 ReentrantLock。

static final class Segment<K,V> extends ReentrantLock implements Serializable {    private static final long serialVersionUID = 2249069246763182397L;    static final int MAX_SCAN_RETRIES =            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;    transient volatile HashEntry<K,V>[] table;    transient int count;    transient int modCount;    transient int threshold;    final float loadFactor;}
final Segment<K,V>[] segments;

默认的并发级别为 16,也就是说默认创建 16 个 Segment。

static final int DEFAULT_CONCURRENCY_LEVEL = 16;

2. size 操作
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。

transient int count;

在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。

ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。

尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。

如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。

static final int RETRIES_BEFORE_LOCK = 2;public int size() {    // Try a few times to get accurate count. On failure due to    // continuous async changes in table, resort to locking.    final Segment<K,V>[] segments = this.segments;    int size;    Boolean overflow;    // true if size overflows 32 bits    long sum;    // sum of modCounts    long last = 0L;    // previous sum    int retries = -1;    // first iteration isn't retry    try {        for (;;) {            // 超过尝试次数,则对每个 Segment 加锁            if (retries++ == RETRIES_BEFORE_LOCK) {                for (int j = 0; j < segments.length; ++j)                                    ensureSegment(j).lock();                // force creation            }            sum = 0L;            size = 0;            overflow = false;            for (int j = 0; j < segments.length; ++j) {                Segment<K,V> seg = segmentAt(segments, j);                if (seg != null) {                    sum += seg.modCount;                    int c = seg.count;                    if (c < 0 || (size += c) < 0)                                            overflow = true;                }            }            // 连续两次得到的结果一致,则认为这个结果是正确的            if (sum == last)                            break;            last = sum;        }    }    finally {        if (retries > RETRIES_BEFORE_LOCK) {            for (int j = 0; j < segments.length; ++j)                            segmentAt(segments, j).unlock();        }    }    return overflow ? Integer.MAX_VALUE : size;}

3. JDK 1.8 的改动

ConcurrentHashMap 取消了 Segment 分段锁。

JDK 1.8 使用 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。

数据结构与HashMap 1.8 的结构类似,数组+链表 / 红黑二叉树(链表长度 > 8 时,转换为红黑树 )。synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 Hash 值不冲突,就不会产生并发。

4. JDK 1.8 中的 put 方法

①. hash 算法

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

②. 定位索引位置

i = (n - 1) & hash

③. 获取 table 中对应索引的元素 f

f = tabAt(tab, i = (n - 1) & hash// Unsafe.getObjectVolatile 获取 f// 因为可以直接指定内存中的数据,保证了每次拿到的数据都是新的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);}Copy to clipboardErrorCopied

④. 如果 f 是 null,说明 table 中是第一次插入数据,利用

⑤. 其余情况把新的 Node 节点按链表或红黑树的方式插入到合适位置,这个过程采用内置锁实现并发。

5. 和 Hashtable 的区别

①. 底层数据结构:

②. 实现线程安全的方式

上述就是小编为大家分享的如何理解Java 容器中并发容器的源码分析了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯