Java集合基础知识(HashMap,ConcurrentHashMap)

2 篇文章 0 订阅

目录

1.HashMap底层原理(JDK1.7,数组+单向链表)

2.HashMap底层原理(JDK1.8,数组+单向链表+红黑树)

3.ConcurrentHashMap底层原理(JDK1.7,Segment分段+可重入锁ReentrantLock)

4.ConcurrentHashMap底层原理(JDK1.8,CAS+synchronized代码块)


1.HashMap底层原理(JDK1.7,数组+单向链表

底层:哈希表(数组+单向链表),数组类型Entry[]

Entry对象包含了4个元素

hashcode,key,value,下一个节点的地址

构造器

默认构造器,初始化数组阈值16,数组长度未知,第一次Put方法设置成16,负载因子赋值0.75(扩容阈值threshold是16*0.75=12)

带参构造器,初始化数组阈值是传递过来的initialCapacity,数组长度未知,第一次Put方法设置成2的指数倍(例如,传递24,实际上是32),负载因子是传递的值threshold,扩容阈值是2的指数倍*threshold

put方法(头插法)

小知识点1:当数组为空的时候,put方法开始初始化数组,长度默认16或者2的指数倍,扩容阈值threshold是12或者2的指数倍乘以负载因子

小知识点2:HashMap允许key的值为null,把null当做一个特殊的key,放到了数组下标是0的位置

  1. 计算hashCode:获取key的hashCode方法,结果进行二次散列和扰动函数,计算出哈希码,尽量保证分散到数组上,减少哈希碰撞。
  2. 计算数组中的下标位置:h & (length - 1),其中h是上一步的hashCode,length是数组长度(默认16),等效于h %(length - 1)的取余函数(与(&)运算效率高)
  3. 哈希冲突(哈希碰撞):hashcode相同,导致计算下标位置相同,如果有元素,比较hashCode和key(==和equals()方法,两者只要有一个是true,则true),如果key相同,则用新Value覆盖老value(只替换value,不替换key),返回老Value,否则遵循7上8下(jdk7头插法,jdk8尾插法)

扩容流程

扩容触发条件有两个:

  1. 当前hashMap的元素个数大于等于阈值的时候
  2. 要插入的数组位置的元素不等于空

扩容流程:

  1. 创建2倍的数组:扩容的新数组Entry空数组,长度是原数组的2倍(默认2*16=32)
  2. 遍历,重新计算所有Key的hashCode和数组下标:两层嵌套循环,先遍历非空的数组元素,然后遍历单个数组元素的链表,判断是否需要重新计算哈希(一般不需要,涉及哈希种子hashseed),结合计算公式,计算出新的数组下标(因为新数组长度变大,key的hashCode计算公式(key.hashCode & (length - 1))也发生了变化),单线程没有问题,多线程下的HashMap在并发情况下的扩容会出现循环链表。所以说HashMap是线程不安全的
  3. 变量重新赋值扩容阈值threshold

2.HashMap底层原理(JDK1.8,数组+单向链表+红黑树

底层:哈希表(数组+单向链表+红黑树),链表和红黑树有可能共存

数组:Node<K,V>[] (这块跟JDK1.7名字不同,本质一样)

红黑树:全称是Red-Black Tree,又称为“红黑树”,它一颗平衡二叉树(左子树跟右子树长度差不多,最长路径不能超过最短路径的二倍)。

特点(算法导论,也叫黑色节点平衡):

  1. 每个节点要么是黑色,要么是红色
  2. 根节点是黑色
  3. 每个叶子节点(NIL)是黑色(叶子节点都是null)
  4. 每个红色结点的两个子结点一定都是黑色
  5. 任意一结点到每个叶子结点的路径都包含数量相同的黑结点
  6. 红黑树并不是一个完美平衡的二叉查找树
  7. 默认插入的节点的初始颜色一定是红色

是否需要旋转或者变色,如下规律:

  1. 如果新加入节点的父节点是黑色的,就不需要调整
  2. 如果父节点是红色,叔叔是空的,需要旋转+变色
  3. 如果父节点是红色,叔叔是红色,需要父节点和叔叔变黑色,祖节变红色
  4. 如果父节点是红色,叔叔是黑色,需要旋转+变色

单向链表的时间复杂度:O(n)

红黑树的时间复杂度:O(logn)

一些关键常量

  • 默认的Node数组长度16(DEFAULT_INITIAL_CAPACITY = 1 << 4)
  • 默认的负载因子是0.75f(DEFAULT_LOAD_FACTOR = 0.75f)
  • 默认的树化阈值是8(TREEIFY_THRESHOLD = 8),树化的意思是单向链表长度大于8的时候,转化成红黑树前提是HashMap所有元素个数大于等于64
  • 默认的树退化阈值是6(UNTREEIFY_THRESHOLD = 6),树化的意思是红黑树的长度小于6的时候,转化成单向链表前提是HashMap所有元素个数小于64
  • 树化的另一个参数是64(MIN_TREEIFY_CAPACITY = 64),前提是HashMap所有元素个数大于等于64
  • Node对象包含了4个元素(这一点跟jdk1.7一样):hashcode,key,value,下一个节点的地址
  • size:当前HashMap已包含的元素个数
  • modCount:当前哈希表的修改次数,替换元素的不算,插入新元素或者减少元素+=modCount
  • threshold:数组长度的阈值

4个构造器(跟jdk1.7基本一样,有点小区别)

对loadFactor和threshold做赋值操作,默认情况下赋值成0.75F和16

不一样的地方是jdk1.7直接赋值,1.8对threshold赋值的时候是取大于initialCapacity并且最小的2的指数倍

put方法

判断1:初始化Node数组(resize()方法中),长度默认是16,这里是懒加载思想,跟jdk1.7一样

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

判断2:计算数组索引,该节点没有Hash碰撞,就意味着是空节点,直接赋值(计算下标的公式: (n - 1) & hash),跟jdk1.7一样

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

判断3:发生了Hash碰撞,如果第一个key相同(这里是比较hashcode和equals),直接覆盖节点操作(无论是链表还是红黑树),跟jdk1.7一样

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

判断4:发生了Hash碰撞,如果第一个key不相同,并且第一个节点的节点类型是红黑树类型,则进行红黑树插入操作,跟jdk1.7不一样(jdk1.7没有红黑树)

else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

判断5:发生了Hash碰撞,如果key不同,并且不是红黑树,那就是链表,则循环遍历链表,进行插入操作,如果插入之前长度大于等于8,则再次插入需要树化操作,跟jdk1.7不一样

如果长度小于8,那就是链表操作,要么找到了相同的key,直接赋值Value,要么插入操作,直接尾插法,跟jdk1.7不一样(jdk1.7是头插法)

else {
	for (int binCount = 0; ; ++binCount) {
		if ((e = p.next) == null) {
			p.next = newNode(hash, key, value, null);
			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;
	}
}

插入新元素之后的size大于阈值,则触发扩容

扩容(跟jdk1.7基本一样,有点小区别,jdk1.7两个条件)

触发条件:是单向链表还是红黑树?满足两个条件,就会转换成红黑树

  1. 单个数组节点上的链表长度大于等于8
  2. 整个HashMap的元素个数大于等于64

红黑树的引用是为了解决链化的问题,换句话说是链表太长,单链表查找影响效率了

3.ConcurrentHashMap底层原理(JDK1.7,Segment分段+可重入锁ReentrantLock

底层:数组Segment+HasMap(哈希表),数组类型Segment[]

原理

要保证HashMap线程安全,最能想到的是Hashtable,把方法都加入synchronized关键字,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,相当于所有线程进行读写时都去竞争一把锁,导致效率非常低下,所以Hashtable废弃了。

  • ConcurrentHashMap原理是对数组进行逻辑切分成一个个n个小数组(比如两个元素,共享一把锁),进行分段加锁,那么就N把锁,能在保证线程安全的情况下,提高效率。
  • ConcurrentHashMap整体来看就是一个Segment数组,每一个Segment对象等效于一个HashMap的对象,每一个HashMap包含了Entry数组和链表
  • 每一个Segment对象,继承了可重入锁ReentrantLock,操作某一个Segment对象需要获取锁,在扩容时也是对某一个Segment对象中的Entry数组进行扩容。
  • 这里总结一下,存在两个类型的数组:一个是Segment数组,一个是Entry数组
  • 默认并发级别(几把分段锁)是16

构造器

无参构造器:

最后Segment数组的大小是16,每一个Segment对象下的Entry数组大小是2,那么阈值是2*0.75

有参构造器(3个参数):

  • initialCapacity:每一个Segment下的Entry数组大小之和,默认是16
  • loadFactor:装载因子,默认是0.75F
  • concurrencylevel:并发级别,就是几把分段锁,也就是Segment个数,也就是Segment数组的大小,默认是16,最大不超过2的16次方65536

1.先初始化Segment数组,大小用S代表,大小是2的指数倍(例如如果concurrencylevel是17,则Segment数组大小是32),一旦Segment数组大小确定后就不会改变了,后续扩容跟Segment数组没有关系,跟Segment中的Entry数组有关系。

2.然后初始化第一个Segment对象中Entry数组,大小是initialCapacity / S = E,并对E向上取整得E1,最后取大于E1并且最小的2的指数倍S1(例如初始容量是17Segment数组的大小是4,那么计算后的数向上取整是5,那么Entry数组的大小就是8

3.将第一个Segment对象放入到Segment数组的第0个位置上去,也就是说只初始化Segment[0]的对象,其他Segment[i]都是空的,底层使用了Unsafe类,调用了C的方法。

Put方法的流程(争取写的细一点)

  1. 判value是否空:先判断key-value中value不能为空,否则空指针异常
  2. 计算Segment数组的下标(计算key的hashcode,通过hashcode计算Segment数组的下标(与HashMap类似,但是有差别,差别在于hashCode需要>>>SegmentShif))
  3. 判断确定位置后的Segment对象是否是空:如果不是空,直接调用Segment.put方法,否则初始化该位置的Segment对象(这里用到了CAS和自旋锁原理
  4. 进入Segment.put方法后,内容跟HashMap的put()方法一样的,这里不写了(这里用到了可重入锁ReentrantLock
  5. 这里只讲锁机制:tryLock尝试去获取Segment锁(跟lock锁的区别是:tryLock是非阻塞加锁,意思是能加锁就返回true,不能加锁就返回false,不强求,lock是阻塞加锁,加不上锁就卡在这),源码中用了while循环,意思是如果获取不到锁,就去干别的事,别闲着,增强效率(为啥要干别的事呢?因为一是别闲着,二是为了减缓while循环速度,不然cpu要爆炸,三是顺便尝试遍历Entry[0]下的链表所有key,获取key的Entry节点,就算遍历完没获取到,也没关系,后续还会for循环遍历所有Entry数组),如果在获取锁的过程中会在偶次数循环中判断头节点是否发生了变化(为啥判断头节点,因为1.7是头插法),如果变化了就从-1开始重新循环Entry[0]下的链表,如果能循环次数到了64次,如果还没获取到锁就直接阻塞等待获取lock()了,获取到锁之后的操作跟HashMap的put()方法一致了。

扩容跟segment数组无关,跟链表无关,跟每一个Segment对象的Entry[]数组有关

这里不多说了,扩容机制跟HashMap一致,区别是ConcurrentHashMap支持多线程扩容,每一个线程扩容1.7都是头插法

4.ConcurrentHashMap底层原理(JDK1.8,CAS+synchronized代码块

底层:数组Node[]+链表+ 红黑树

构造器

默认构造器:空,啥也没干,默认Node数组长度16,加载因子0.75f,并发

有参构造器:对sizeCtl变量做赋值操作

sizeCtl变量的含义:

  • 0:默认值
  • -1:正在创建数组
  • -N:表示有N-1个线程正在进行扩容操作
  • N:当前数组下一次扩容的阈值

Put方法的流程

  1. 检查Key或者Value是否为null,为空,抛出空指针异常
  2. 计算key的hash值((h ^ (h >>> 16)) & 0x7fffffff)
  3. 如果Node数组是空的,此时才初始化 initTable()
  4. 如果找的对应的下标的位置为空,使用CAS方式直接new一个Node节点并放入, break;
  5. 如果对应头结点不为空, 进入同步代码块,Syschronized锁住首结点f
  6. 如果首节点f的hash值等于-1,说明其他线程在扩容,参与一起扩容
  7. 首节点f的hash值大于零则说明是链表的头结点,则链表插入,在链表中遍历寻找,如果有相同hash值并且key相同,就直接覆盖,返回旧值 结束;如果没有则就直接放置在链表的尾部,
  8. 如果首节点f instanceof TreeBin(或者小于0且不等于-1),则说明是红黑树的根节点,则红黑树插入,则说明此节点是红黑二叉树的根节点,调用树的添加元素方法
  9. 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8,这里需要注意它不是一定会进行红黑树转换, 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树

get方法的流程

  1. 计算 hash 值
  2. 根据 hash 值找到数组对应位置: (n - 1) & h
  3. 根据该位置处结点性质进行相应查找
  • 如果该位置为 null,那么直接返回 null 就可以了
  • 如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
  • 如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树
  • 如果以上 3 条都不满足,那就是链表,进行遍历比对即可

如何保证线程安全的

抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁

只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的数组元素的读写,大大提高了并发度。

JDK1.7 与 JDK1.8 中ConcurrentHashMap 的区别?

  • 数据结构:jdk1.7采用Segment 数组+数组+链表的数据结构,jdk1.8是数组+链表+红黑树的数据结构。
  • 线程安全机制:JDK1.7 采用 Segment 的分段锁(每一个分段锁都是可重入锁ReentrantLock机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8 采用CAS + synchronized保证线程安全。
  • 锁的粒度:JDK1.7 是对其中一个Segment加锁ReentrantLock,JDK1.8 调整为对Node数组的首节点进行加锁synchronized,明显看出jdk1.8锁的粒度更细致。
  • 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash 冲突加剧,因此在链表节点数量大于等于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
  • 查询时间复杂度:从 JDK1.7的遍历链表O(n)JDK1.8 变成遍历红黑树O(logN)

ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?

因为 ConcurrentHashMap 是用于多线程的 ,如果ConcurrentHashMap.get(key)得到了 null ,这就无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,就有了二义性。

ConcurrentHashMap 迭代器是弱一致性,HashMap迭代器是强一致性

与 HashMap 迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。

ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
应用背景为变电站电力巡检,基于YOLO v4算法模型对常见电力巡检目标进行检测,并充分利用Ascend310提供的DVPP等硬件支持能力来完成流媒体的传输、处理等任务,并对系统性能做出一定的优化。.zip深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彼岸花@开

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值