【源码分析】一文看透集合容器

一文看透集合容器

  • 一、Map
    • a. HashMap
    • b.ConcurrentHashMap
    • c.HashTable
    • d. TreeMap
  • 二、Collection
    • a. List
      • ArrayList
      • LinkedList
      • Vector
      • CopyOnWriteArrayList
      • 对比和自身思考
        • 思考:为什么都拒绝使用Vector啊?它线程安全诶
    • b. Set
      • HashSet
      • TreeSet
      • CopyOnWriteArraySet
    • c. BlockingQueue
      • ArrayBlockingQueue
      • LinkedBlockingQueue
      • 对比和自身思考
        • 思考:为什么 LinkedBlockingQueue 中记录元素个数要使用 AtomicInteger 类型,为啥要CAS操作?
  • 三、Collections#shuffle

本篇博客会对我画的如下图的集合进行源码分析,去发现新大陆:

在这里插入图片描述

一、Map

a. HashMap

经典集合,以1.8展开说,内部通过链表/红黑树的方式解决hash冲突问题,通过扰动计算计算hash值去降低hash冲突的概率,通过&运算代替%取模,加快计算速度…

put 方法解析如下:

		// hash值的计算
		// 将高位的hash码和低位hash码混合计算,降低hash冲突概率
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
		
	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        // resize 初次扩容
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        // hash映射到hash表对应位置是空,直接new
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 判断是否key相同,相同进行覆盖
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                // 判断是否是红黑树,是进行红黑树的添加操作
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                // 链表尾部插入
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 链表长度到了8重构为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 判断是否去映射值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 判断是否需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

b.ConcurrentHashMap

这里不给你分析jdk1.7用Segment然后继承ReentrantLock去实现分段锁保证线程安全的,咱直接步入主题,看1.8是咋使用分段思想+Synchronized和CAS去保证线程安全的,这里的分段思想是说它锁的时候只是锁头节点,而不是说给整个Map上锁,也不是说向1.7那样分段,只能说锁的粒度小了。

直接步入主题,分析源码,put、get

public V put(K key, V value) {
    return putVal(key, value, false);
}


public V putVal(K key, V value) {
    if (value == null) // 可以看见,为空的话直接抛出空指针异常,所以说 ConcurrentHashMap是不能存空的
        throw new NullPointerException();

    // 对 key 的 hashCode 进行扰动
    int hash = spread(key.hashCode());
    int binCount = 0;

    // 循环操作
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;

        // 如果 table 为 null 或长度为 0,则进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();

            // 如果哈希槽为空,则通过 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;
        }

            // 如果哈希槽处已经有节点,且 hash 值为 MOVED,则说明正在进行扩容,需要帮助迁移数据
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);

            // 如果哈希槽处已经有节点,且 hash 值不为 MOVED,则进行链表/红黑树的节点遍历或插入操作
        else {
            V oldVal = null;

            // 加锁,确保只有一个线程操作该节点的链表/红黑树
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        // 遍历链表,找到相同 key 的节点,更新值或插入新节点
                        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;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                // 将新节点插入到链表末尾
                                if (casNext(pred, new Node<K,V>(hash, key,
                                                                value, null))) {
                                    break;
                                }
                            }
                        }
                    }
                        // 遍历红黑树,找到相同 key 的节点,更新值或插入新节点
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        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)
                    treeifyBin(tab, i);
                // 如果替换旧值成功,则返回旧值
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //

    addCount(1L, binCount);
    return null;
}

当table是空的时候,初始化table的时候也是通过CAS去控制首次扩容的:

在这里插入图片描述
get 方法解析,就是直接取:

在这里插入图片描述并发控制阶段:

  • 初始化桶阶段,CAS自旋去让某线程去初始化,防止多线程初始化导致的资源浪费;
  • put元素阶段,若是直接头部插入那就CAS,若是插入链表和红黑树就用Synchronized锁住头部;
  • 扩容阶段。

c.HashTable

该集合及其的简单,内部是通过单链表去解决hash冲突的,没有引入红黑树,而且它计算hash很直接,就是使用hashCode方法,比较特殊点就是它是线程安全的,在操作的方法前都加了synchronized关键字保障线程安全。
下面是put源码分析:

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        // HashTable也不允许存储Null值
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        // 去尝试是否替换值
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
				
				// 向链表尾部添加元素,判断是否需要扩容
        addEntry(hash, key, value, index);
        return null;
    }

get 方法就更简单了,直接就是遍历找对应节点就好了:

在这里插入图片描述

d. TreeMap

咱先看一下它节点Entry的定义:
在这里插入图片描述妥妥红黑树节点,明着说它底层就是一个红黑树,添加、查找元素都是对红黑树进行操作,想看红黑树咋实现的,这个TreeMap就是很好的参考。

这需要讲讲它的不同点,它的话当我们去遍历它的时候(不管是用keySet、entrySet…),它最后的结果都是有序的,它内部是通过迭代器去操作的,首先是得到红黑树最左的结点,也就是最小值的节点,然后再通过迭代器不断迭代…总结的说就是这个集合是有序的,排序规则的话默认是由存入的数据key的比较器为准,当然也可在构造TreeMap的时候自己指定。

二、Collection

a. List

主要是对 add、get、remove 方法进行分析

ArrayList

它不考虑线程安全问题,底层基于数组实现起来真的很简单,谁懂啊?内部封装了个elementData 数组,起初是一个空数组,第一次添加元素会扩容十,谁后满了再添加会扩容1.5倍,扩容起来很简单就是Arrays.copyOf,内部就是创建数组然后调用System.arraycopy去操作。那get、remove方法实现就是基于数组的基本实现。

add 方法实现,检查容量,容量不够调用 grow 进行扩容,然后填充元素:
在这里插入图片描述remove
在这里插入图片描述get
在这里插入图片描述

LinkedList

大伙应该都知道这玩意底层是个双向链表,但是这里想说的是它除了实现了List接口,还实现了Deque接口,就是说它具有双端队列的功能,提供了offerFirst、offerLast方法,默认操作都是个队列操作,即add是向尾添加,remove是向表头删除,getFirst是拿表头元素。

简单看看add、remove、get方法吧,不难,就是链表节点之间的连接、删除操作。
add 分析:
在这里插入图片描述在这里插入图片描述remove分析:
在这里插入图片描述在这里插入图片描述getFirst分析:

在这里插入图片描述

Vector

Vector底层实现和ArrayList没啥区别,都是基于数组,只不过Vector在grow扩容的时候是按俩倍扩容,而且它初始化容量是10,而ArrayList是没给初始化容量的,只不过第一次扩容是10.还有最大的区别就是它 add、remove、get 方法都加了 synchronized 关键字保障线程安全。

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

CopyOnWriteArrayList

底层实现还是基于数组的,但是它可以说不是动态数组,紧紧滴。内部有个 ReentrantLock 锁,在对数组进行写操作的时候会进行上锁。但是添加操作是先扩容原长度+1/-1,然后再进行添加/删除元素的。就只要写操作就会涉及Arrays.copyOf扩容/缩容现象。
在这里插入图片描述
add 方法实现,上锁,扩容,添加元素,OK。

在这里插入图片描述remove 方法也简单,上锁,System.arraycopy 移除元素。
在这里插入图片描述
get 的话直接获取对应值就好了:

在这里插入图片描述

对比和自身思考

LinkedList可以作为双端队列使用,它底层是双向链表;
ArrayList、Vector、CopyOnWriteArrayList底层都是数组,ArrayList是非线程安全的,后俩是线程安全的。

思考:为什么都拒绝使用Vector啊?它线程安全诶

个人觉得哈,Vector作为一个动态数组容器,虽然保障了线程安全,但是对于这种数组容器,是经常会有读取操作的,那在高并发的情况下,读操作会因写操作而阻塞?那这往往是不能接受的,这也是为什么而后引出了 CopyOnWriteArrayList 集合的原因吧

b. Set

HashSet

底层直接根据HashMap进行操作,Easy。
在这里插入图片描述添加操作就是往 HashMap 中添加元素,value 就是内部封装的一个Object 对象,final的。
在这里插入图片描述遍历起来也是根据HashMap 的keySet进行遍历的:

在这里插入图片描述

TreeSet

内部基于 TreeMap,emmmm…
在这里插入图片描述添加元素就是往这个 TreeMap 中添加元素:
在这里插入图片描述这里要阐述的是一个 tailSet 方法,会重构一个AscendingSubMap,重构一个Set出来在这里插入图片描述
然后我们掉这个 tailSet 得到的集合的 first 方法的时候,就可以拿到拿到大于等于刚刚传进去的 fromElement 的第一个key:

在这里插入图片描述

CopyOnWriteArraySet

内部是由 CopyOnWriteArrayList 实现的,鼠鼠震惊~

在这里插入图片描述它的add方法:

在这里插入图片描述这添加就得 O(n) 时间复杂度了,它去遍历数组去判断这个元素是否存在的…虽保障了线程安全,但是比起 HashSet 性能要降不少。

c. BlockingQueue

主要分析三个方法实现:peek(读取队头元素)、poll(删除队头元素,并返回)、offer(入队)

ArrayBlockingQueue

底层是基于数组进行实现,由ReentrantLock保障线程安全(默认使用非公平锁),由takeIndex、putIndex表示出队头和队尾,用于插入元素,count记录队元素个数。
在这里插入图片描述offer 方法实现,很简单,加锁,向队尾添加元素,putIndex、count加1,putIndex如果等于总容量了就置为0:
在这里插入图片描述poll方法实现,一样简单,加锁,随后根据takeIndex让队头元素删除,一样takeIndex、count都加1,takeIndex到容量了置为0:

在这里插入图片描述
peek实现更简单,加锁取队头元素:
在这里插入图片描述
可以发现 poll、peek、offer 这三操作都是使用同一把锁,都是构造时候创建的ReentrantLock锁,就是说线程在执行这三方法时都存在竞争行为。

LinkedBlockingQueue

底层基于单链表进行实现的,由head节点属性指向队头,由last节点属性指向队尾,还于ArrayBlockingQueue不同的是LikedBlokingQueue内部维护了俩把ReentrantLock锁,一个putLock锁用来在控制队尾写操作的安全,一个takeLock锁用来控制队头读写操作的安全。这个时候在记录总元素的时候就可能存在线程安全问题,所以使用了原子性AtomicInteger,用CAS操作去保障队列元素总数变量的线程安全问题。
在这里插入图片描述
offer 方法实现,取队尾读写锁 putLock,进行加锁,队尾添加元素,count.getAndIncrement() CAS 进行队内元素个数+1.
在这里插入图片描述
poll 方法实现,拿到队头锁 takeLock,进行加锁,队头删除元素,count.getAndDecrement() CAS 进行队内元素个数-1.

在这里插入图片描述
peek 方法实现,取对应的队头锁takeLock,加锁,然后取头部元素就是了:

在这里插入图片描述

对比和自身思考

ArrayBlockingQueue 中三个操作都是使用同一把锁,且记录元素个数是long类型,反观 LinkedBlockingQueue 中有俩把锁,一把用于队尾操作,一把用于队头读写操作,而记录元素个数的是 AtomicInteger 类型,从这可以看见的后者的锁的粒度是更小的,那并发下性能也会更好。

思考:为什么 LinkedBlockingQueue 中记录元素个数要使用 AtomicInteger 类型,为啥要CAS操作?

这问题是开始我看LinkedBlockingQueue源码的时候,以为它和ArrayBlockingQueue一样使用的是同一个ReentrantLock对象,就是说操作前后都是上锁的,那么记录元素个数为什么要用AtomicInteger这种原子类型呢?后来仔细看发现是队头和队尾的操作是使用的不同的锁,那么这之间对元素个数变量就存在线程安全问题,而这里处理该线程安全就是使用原子类型的CAS操作进行的。完美~

三、Collections#shuffle

Collections 工具类里面有个 shuffle 方法,用来随机排序内部的元素的,咱来看看它咋实现的:
在这里插入图片描述遍历一遍元素,从末尾开始,随机找前面序列的一个数进行交换,从而达到随机排序的效果。你别说,还挺有意思~😮😮😮

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/496424.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024年【烟花爆竹产品涉药】免费试题及烟花爆竹产品涉药考试技巧

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【烟花爆竹产品涉药】免费试题及烟花爆竹产品涉药考试技巧&#xff0c;包含烟花爆竹产品涉药免费试题答案和解析及烟花爆竹产品涉药考试技巧练习。安全生产模拟考试一点通结合国家烟花爆竹产品涉药考试最新大纲…

135.分发糖果

javapublic class Solution {public int candy(int[] ratings) {// 获取孩子人数int len ratings.length;// 初始化一个数组存储每个孩子的糖果数&#xff0c;默认第一个孩子有1颗糖果int[] candyVec new int[len];candyVec[0] 1;// 阶段1&#xff1a;从左到右遍历for (int …

MongoDB内存过高问题分析解决

告警 公司有个3.2.7版本的mongo复制集&#xff0c;最近几天频繁告警内存过高。 服务器配置16C64G内存。mongo备节点内存使用到55G&#xff0c;触发告警。 以下内容基于3.2.7版本&#xff0c;3.2.7版本已经太老&#xff0c;很多后来的命令和配置&#xff0c;3.2.7都没有。 …

C++自主点餐系统

一、 题目 设计一个自助点餐系统&#xff0c;方便顾客自己点餐&#xff0c;并提供对餐厅销售情况的统计和管理功能。 二、 业务流程图 三、 系统功能结构图 四、 类的设计 五、 程序代码与说明 头文件1. SystemMap.h #pragma once #ifndef SYSTEMMAP #define SYSTEMMAP #in…

vue3全局引入element-plus使用Message教程

文章目录 安装引入 Element Plus和组件样式示例注意安装与引入&#xff1a;按需引入&#xff1a;API 使用&#xff1a;样式问题&#xff1a;组件上下文&#xff1a;版本兼容性&#xff1a;错误处理&#xff1a; 这是 Element UI 的 Vue 3 版本。ElMessage 是 Element Plus 中的…

在Linux上使用nginx反向代理部署Docker网站

在政务云上部署Web环境&#xff0c;为了保证服务器安全&#xff0c;甲方只开放一个端口且只允许使用https协议进行访问&#xff0c;经过思考&#xff0c;决定使用docker部署网站&#xff0c;使用nginx反向代理&#xff0c;通过不同的二级域名访问不同的端口。 1 使用docker部署…

编程语言|C语言——C语言变量的存储方式

前言 变量是程序中数据的存储空间的抽象。变量的存储方式可分为静态存储和动态存储两种。 静态存储变量通常是在程序编译时就分配一定的存储空间并一直保持不变&#xff0c;直至整个程序结束。在上一部分中介绍的全局变量的存储方式即属于此类存储方式。 动态存储变量是在程序执…

超越极限!《无名之辈》高阶武学与战术应对策略一览!

欢迎来到《无名之辈》世界&#xff01;在这里&#xff0c;决战不仅需要勇气&#xff0c;更需要智慧和策略。为了让你在游戏中游刃有余&#xff0c;以下是一份全面的游戏攻略&#xff0c;助你成为战场上的无敌之王&#xff01; 一、主角战斗技巧&#xff1a; 反击属性至关重要&a…

Vue3状态管理库--Pinia

Pinia快速入门 一、什么是Pinia &#xff1f; Pinia 是 Vue 的专属的最新状态管理库 &#xff0c;是 Vuex 状态管理工具的替代品。 Pinia官网链接 提供更加简单的API &#xff08;去掉了 mutation &#xff09;提供符合组合式风格的API &#xff08;和 Vue3 新语法统一&…

2024年【低压电工】实操考试视频及低压电工考试试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 低压电工实操考试视频是安全生产模拟考试一点通生成的&#xff0c;低压电工证模拟考试题库是根据低压电工最新版教材汇编出低压电工仿真模拟考试。2024年【低压电工】实操考试视频及低压电工考试试题 1、【单选题】()…

【C++实验1】学生成绩信息管理系统题解

【问题描述】编写一个基于结构体得学生成绩信息管理系统。 主要功能如下&#xff1a; 1. 用结构体存放所有数据。 2. 每个功能都用函数实现。 3. 输入10个学生的学号和三门课程的成绩。 4. 计算每个学生的总分。 5. 按总分从高到低排序。 6. 加上名次一列。 7. 输出最后…

ssm婚纱摄影管理系统的设计+1.2w字论文+免费调试

项目演示视频&#xff1a; ssm婚纱摄影管理系统的设计 项目介绍: 随着现在网络的快速发展&#xff0c;网上管理系统也逐渐快速发展起来&#xff0c;网上管理模式很快融入到了许多商家的之中&#xff0c;随之就产生了“婚纱摄影网的设计”&#xff0c;这样就让婚纱摄影网的设计更…

【微服务】Nacos(注册中心)

文章目录 1.基本介绍1.概述2.Nacos下载和运行&#xff08;java8/maven3.2.x&#xff09;1.解压到没有中文路径的2.双击startup3.浏览器输入http://192.168.242.124:8848/nacos4.用户名和密码为nacos5.cmd输入netstat -anb | more查看监听端口 2.创建Nacos服务提供者 100041.项目…

springboot实战---4.常用内容小结

&#x1f388;个人主页&#xff1a;靓仔很忙i &#x1f4bb;B 站主页&#xff1a;&#x1f449;B站&#x1f448; &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;SptringBoot &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处…

【项目技术介绍篇】若依管理系统功能介绍

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过大学刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0…

【scala】使用gradle和scala构建springboot程序

零、版本说明: springboot: 2.7.18 使用log4j2&#xff0c;不使用springboot自带的logback scala版本&#xff1a;2.11 jackson版本&#xff1a;2.16.0 一、依赖&#xff1a; buildscript {dependencies {// using spring-boot-maven-plugin as package toolclasspath("…

Scala第十三章节(作为值的函数及匿名函数、柯里化、闭包及控制抽象以及计算器案例)

章节目标 掌握作为值的函数及匿名函数的用法了解柯里化的用法掌握闭包及控制抽象的用法掌握计算器案例 1.高阶函数介绍 Scala 混合了面向对象和函数式的特性&#xff0c;在函数式编程语言中&#xff0c;函数是“头等公民”&#xff0c;它和Int、String、Class等其他 类型处于…

【华大 HC32L110】调用`printf`和串口接收中断的冲突问题解决

华大单片机 HC32L110调用printf和串口接收中断的冲突问题解决,经过查找是官方库 去使能了 串口的接收功能,记录解决问题的过程 目录 1.硬件MCU资料2. printf和串口接收中断的冲突解决3.重新封装 fputc 函数4.查找问题,发现是官方库配置有误5. 查找寄存器手册,修改寄存器配置…

智慧光伏:企业无纸化办公

随着科技的快速发展&#xff0c;光伏技术不仅成为推动绿色能源革命的重要力量&#xff0c;更在企业办公环境中扮演起引领无纸化办公的重要角色。智慧光伏不仅为企业提供了清洁、可持续的能源&#xff0c;更通过智能化的管理方式&#xff0c;推动企业向无纸化办公转型&#xff0…

MySQL三种开窗函数详细用法,图文详解

开窗函数的详细用法 第一章、开窗函数的语法1.1&#xff09;从聚合开窗函数讲起1.2&#xff09;开窗函数之取值1.3&#xff09;排名开窗函数 第一章、开窗函数的语法 开窗函数的语法为&#xff1a;over(partition by 列名1 order by 列名2 )&#xff0c;括号中的两个关键词par…