集合{LinkedHashMap TreeMap HashSet LinkedHashSet TreeSet 快速失败机制 ConcurrentHashMap CAS 多线程协同扩容}(二)

目录标题

LinkedHashMap

Map集合框架结构体系图

在这里插入图片描述

在这里插入图片描述

什么是LinkedHashMap
Linked 链式 的意思
HashMap “哈希映射”的意思
LinkedHashMap 链式哈希映射


它扩展自HashMap
并且 用双向链表来保证元素的顺序
Entry<K,V> before, after;
还记录了头节点和尾节点


在这里插入图片描述

重点
也就是说
它是有序的
HashMap是无序的
这也是它俩最大的区别


在这里插入图片描述

维护元素顺序的方式有两种
—种是“添加顺序”
—种是“访问顺序”



什么是“添加顺序”


添加顺序:
元素添加的顺序和取出的顺序一致


例如
我们依次添加
苹果 香蕉 梨子 葡萄 柠檬
取出的顺序:
从头节点开始
苹果 香蕉 梨子 葡萄 柠檬


在这里插入图片描述

练习

程序验证:
从执行结果来看
元素添加的顺序和取出的顺序一致


在这里插入图片描述

什么是访问顺序


什么是访问顺序:
被访问过的元素排在最后
通俗来讲
就是 get方法获取谁
谁就排到最后


在这里插入图片描述


举例:
如下图
我们获取了苹果
那么苹果就排到最后去
成为了尾节点
香蕉就自然成为了头节点
此时取出的顺序是
香蕉 梨子 葡萄 柠檬 苹果



在这里插入图片描述




通过accessorder属性
可以设置排序方式
false是添加顺序
true是访问顺序


在这里插入图片描述



LinkedHashMap一共有5个构造方法
其中有4个默认是false
只有一个构造方法
可以自主选择


在这里插入图片描述

验证访问顺序的代码
从执行结果来看
被访问的元素的确排在最后


练习

在这里插入图片描述

LinkedHashMap常用方法

LinkedHashMap 的常用方法有哪些
它既继承了HashMap
又实现了Map接口


在这里插入图片描述

所以
它拥有 Map和HashMap 里面的所有方法


Map方法分类和HashMap方法分类

在这里插入图片描述


但是在这些方法里面
只有八个方法是最常用的
其中
put和get方法用的最频繁


在这里插入图片描述

LinkedHashMap总结

在这里插入图片描述


它最大的优点是有序
这也是它和 HashMap最明显的区别、
在实际开发中
需要有序的Map集合时使用它


在这里插入图片描述

TreeMap集合

在这里插入图片描述

Map集合框架结构体系图

在这里插入图片描述

Tree 树的意思 这里的树 是指"二叉树"
Map "映射"的意思
TreeMap 二叉树映射的意思


重点!
可以看出来
它是以二叉树形式存储数据的
而且二叉树的类型是红黑树
红黑树拥有自平衡的特点
它会根据key进行排序


在这里插入图片描述

TreeMap 对key排序(红黑树的性质)


要了解排序过程
我们先要知道红黑树的五个性质
第一个性质
每个节点非黑即红
第二个性质
根节点是黑色
第三个性质
叶节点是黑色
第四个性质
每个红节点下的子节点是黑色
第五个性质
任意节点到叶节点的
每一条路径上的黑色节点数相同



在这里插入图片描述




我们向TreeMap集合中
依次添加E-A五个元素
最开始添加的是E
它是根节点 是黑色的
接下来添加的是D
它需要先跟E进行比较
结果小于 0 的话
放在E 的左边
并把它涂红
再接下来添加的是 C
它小于E
也小于D
放在D的左边
并把它涂红
此时 此时D和C两个父子节点都是红色
不满足红黑树性质四
每个红节点下的子节点是黑色
而D的子节点C 是红色



因此我们需要对其进行调整
先将C的父节点D 涂黑
然后再将C的祖父节点E 涂红
接着对E进行右旋
此时的红黑树
满足所有性质



再接下来添加的是B
它小于D 也小于C
放在C的左边
并把它涂红
此时C 和B 都是红色
同样不满足红黑树性质四
因此需要对其进行调整




先将B的父节点 C 涂黑
再将B的祖父节点D的右子节点E 也涂黑
此时的红黑树 满足所有性质



最后添加的是A
它小于D 也小于 C 也小于B
放在B的左边 并把它涂红
此时B和A都是红色
同样不满足红黑树性质四



因此需要对其进行调整
先将A的父节点 B 涂黑
再将A的祖父节点C 涂红
接着对C进行右旋
此时的红黑树
满足所有性质
到此 所有元素已经添加完毕



在这里插入图片描述

在这里插入图片描述

重点!
TreeMap遍历元素的方式
是中序遍历
先左节点
再根节点
最后右节点



在这里插入图片描述

练习

程序验证
从执行结果来看: 的确是中序遍历的方式
输出 A-E


在这里插入图片描述

TreeMap的特点 :无序

元素添加的顺序: 和取出的顺序不一致


在这里插入图片描述

TreeMap的特点 :key不可为null

重点!
我们在添加元素的地方
发现了 key 不可以为 null
如果为 null
则会抛出NullPointerException(空指针异常)


在这里插入图片描述

另外 null无法参与比较
由此我们可以总结出
TreeMap的第三个特点:
key不可为null



在这里插入图片描述

TreeMap的特点 :key唯一

我们在比较两个key的地方发现
当比较结果为 0 时
说明两个key相同
走else 分支
新值直接覆盖掉 旧值
由此我们可以总结出:
TreeMap的第四个特点:
key不可以重复
也就是key唯一



在这里插入图片描述

TreeMap的常用方法


重点!
TreeMap 实现了Map接口
还实现了NavigableMap接口
增强了搜索元素的能力
NavigableMap继承自SortedMap接口
让TreeMap有了排序的能力
最后:SortedMap继承 Map接口
拥有最基本的Map能力


在这里插入图片描述

综上所述
TreeMap拥有Map SortedMap NavigableMap
里面的所有方法


TreeMap拥有Map SortedMap NavigableMap所有方法

但是在这些方法里面
它只有九个方法是常用的


在这里插入图片描述

总结

在这里插入图片描述

重点!
在实际开发中
它是用的最频繁的容器之一
多用于需要对key排序的集合


在这里插入图片描述

HashSet集合

在这里插入图片描述

Collection集合框架结构体系图

在这里插入图片描述


Hash 散列 或者 "哈希"的意思
set 集合的意思
HashSet 哈希集合的意思
它内部用的是 HashMap 来储存数据



在这里插入图片描述

所以看出
为什么HashSet是无序集合
它的四个构造方法无一例外
全部都在初始化HashMap
也就是说
我们创建 HashSet
其实就是创建了一个HashMap



在这里插入图片描述

存数据的时候
只需要把 key 当 value使
value再用一个统一的值填充即可
这个值就定义在HashSet的内部
名叫“PRESENT”
是Object 类型



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


元素取出的顺序
如果是数组的话
从下标0 开始
如果是链表的话
从头节点开始
依次是
柠檬 苹果 香蕉 葡萄 梨子



在这里插入图片描述

练习


程序验证:
从执行结果来看 和我们刚刚说的顺序 一样
由此 我们可以总结出
HashSet的第一个特点:无序


在这里插入图片描述

元素存入的顺序和取出的顺序不一致


在这里插入图片描述

其他的特点以及优缺点
都和HashMap一样

在这里插入图片描述

HashSet 的常用方法

HashSet 实现了Set接口
拥有Set 里面的所有方法


但是在这些方法里面
它只有七个方法是最常用的
重点!!!
add和iterator方法用得最频繁



在这里插入图片描述

HashSet 与 HashMap 的区别

因为HashSet 内部用的就是HashMap
所以它俩只有一个区别
那就是 HashMap : key是key value是value
而 HashSet :把key当value 使用 value再用统一的值填充



在这里插入图片描述

总结:

介绍了 无序集合 HashSet

在这里插入图片描述

在实际开发中
它是用得最频繁的容器之一
多用于保存不重复的数据


在这里插入图片描述

LinkedHashSet

在这里插入图片描述

Collection集合框架结构体系图

在这里插入图片描述

Linked "链式"的意思
HashSet "哈希集合"的意思
LinkedHashSet :链式哈希集合
所以可以看出
它扩展自 HashSet
但它内部使用的是LinkedHashMap
LinkedHashMap是有序的




在这里插入图片描述

所以
LinkedHashSet 也是有序的
关于LinkedHashMap更多内容
在之前学过 就不介绍了
当我们使用LinkedHashSet存数据的时候
只需要把 key 当 value 使
value再用一个统一的值填充即可
这个值就定义在HashSet的内部



在这里插入图片描述

元素取出的顺序
从头节点开始
依次是 苹果 香蕉 梨子 葡萄 柠檬


在这里插入图片描述

练习

程序验证:
从执行结果来看
和我们刚刚说的顺序一样
由此
我们可以总结出
LinkedHashSet的第一个特点
有序




在这里插入图片描述

重点!
元素存入的顺序 和 取出的顺序 一致
其他的特点 及优缺点
都和 LinkedHashMap一样


在这里插入图片描述

LinkedHashSet 常用方法

LinkedHashSet继承自HashSet
实现了 Set接口


在这里插入图片描述

所以 : 它拥有Set HashSet 里面的所有方法
但是 在这些方法里面
它只有七个方法是最常用的
其中add和 iterator方法用得最频繁



Set方法分类 HashSet方法分类 LinkedHashSet 常用方法分类

在这里插入图片描述

LinkedHashSet 与 LinkedHashMap的区别



两者有两个区别
第一个区别: 存储方式的不同
LinkedHashMap : key是key value是 value
而 LinkedHashSet: 把key当 value 使用 value 再用统一的值填充


第二个区别是: 排序方式的不同
LinkedHashMap有两种不同的排序方式
一种是 : 添加顺序
一种是 : 访问顺序



而LinkedHashSet只有一种排序方式
那就是 添加顺序 也就是默认的取出顺序
这点大家需要注意!



总结:

本节介绍了LinkedHashSet
它的特点及 优缺点


在这里插入图片描述

重点!
在实际开发中
它是用的最频繁的容器之一
多用于保存不重复且有序的数据


在这里插入图片描述

TreeSet

在这里插入图片描述

Collection集合框架结构体系图

在这里插入图片描述

Tree “树"的意思 这里的树 是指"二叉树”
Set :"集合"的意思
TreeSet: 二叉树集合 的意思



它内部使用的是TreeMap
TreeMap是基于红黑树实现的
红黑树拥有自平衡的特点
它会对元素进行排序
默认的排序规则是自然排序
自然排序是通过元素的compareTo方法完成的
自定义排序则需要实现Comparator接口
遍历元素的方式 : 采取的是 中序遍历
先左节点 再根节点 最后右节点



在这里插入图片描述

在这里插入图片描述

练习


程序验证:
从执行结果来看
和我们刚刚说的顺序一样
由此
我们可以总结出
TreeSet的第一个特点:
无序


在这里插入图片描述

元素存入的顺序和取出的顺序不一致
其他的特点以及优缺点
都和TreeMap 一样
关于TreeMap 的学习内容在之前讲解过


在这里插入图片描述

TreeSet 常用方法

TreeSet继承自AbstractSet抽象类
实现了Set接口
TreeSet 还实现了NavigableSet接口
增强了搜索元素的能力
NavigableSet继承自SortedSet接口
让TreeSet有了排序的能力
最后
SortedSet继承自Set接口



TreeSet 结构体系图

在这里插入图片描述

所以
TreeSet 拥有 Set SortedSet
NavigableSet 里面的所有方法


Set方法分类 SortedSet方法分类 NavigableSet常用方法分类

在这里插入图片描述

但是这些方法中
它只有 八个 方法是最常用的
其中add和iterator方法用得最频繁


在这里插入图片描述

比较HashSet和LinkedHashSet




在实际开发中
HashSet LinkedHashSet TreeSet
之间该如何选择


HashSet的性能
基本上比 LinkedHashSet和TreeSet要好
特别是在 添加 和 查询 操作
而这两个操作也是用得最频繁的操作



LinkedHashSet查询稍慢一些
但它 可以维持 元素 添加的顺序
所以 只有当 需要 存入的顺序 和取出的顺序 一致 时
才应该使用LinkedHashSet



TreeSet只有在需要对元素进行排序时使用
所以以下是
三种Set集合各自的应用场景


在这里插入图片描述

总结 TreeSet

它的特点以及优缺点

在这里插入图片描述

在这里插入图片描述

在实际开发中
它是用的最频繁的容器 之一
多用于有排序需求的Set 场景



在这里插入图片描述

快速失败(fail-fast)机制

在这里插入图片描述

在集合中有一种保护机制
叫 fail-fast
翻译过来就是
fail-fast : 快速失败
它能够防止多个线程并发修改同一个容器的内容
如果发生了并发修改的情况
那么就会触发“快速失败”的机制



在这里插入图片描述

也就是抛出并发修改异常

在这里插入图片描述

例如
你在迭代遍历某个容器的过程中
另一个线程介入其中
并且插入或者删除此容器内的某个元素
那么就会出现问题
不光是多线程会出现问题
就连单线程也会出现问题



在这里插入图片描述



重点!
下面 通过一个过程
来复现以下问题是怎样产生的
试想一下
当我们的容器正在扩容时
突然有两个元素被添加进来
请问 : 它们该放在哪里



放在旧容器不合适
再者说 旧容器也容不下它们
只能放在新容器
2 号位 是梨子的
3 号位 是葡萄的
所以它们只能往后顺位
由于它们是同一时刻被添加进来的
导致它们计算的位置(下标)是一样的
这就产生了
同一个位置出现了两个元素的情况




在这里插入图片描述

重点!
问题也就显示了出来
为了防止这样的问题出现
Java容器类采用了快速失败(fail-fast)机制
它会监视容器的变化
具体的做法是
在容器类中
定义一个modCount 属性
用于记录 容器修改 次数
初始值为 0



在这里插入图片描述

只要容器有添加或删除元素的操作
modCount就 ++


在这里插入图片描述



在每次获取迭代器的时候
迭代器会先读取一次modCount的值
迭代的时候进行比较



如果两个值不相等
那么就说明有其他线程修改了容器
导致数据混乱
因此抛出并发修改异常
以上就是“快速失败(fail-fast)机制”的原



在这里插入图片描述

快速失败 解决方案


接下来 来看看如何避免上述问题
会发生上述问题的容器
说明它们都是线程不安全的
所以
只要采用对应的线程安全的容器即可



例如:
ConcurrentHashMap
CopyOnWriteArrayList
和 CopyOnWriteArraySet
都可以



在这里插入图片描述

拿CopyOnWriteArrayList举例来说
先创建一个CopyOnWriteArrayList集合
然后启动另一个线程来修改容器
往容器中添加数据
最后获取迭代器
遍历集合




在这里插入图片描述

执行程序:
从执行结果来看
程序没有发生异常
并打印出了刚刚添加的数据


在这里插入图片描述

说明使用线程安全的容器类
可以避免并发修改的问题


在这里插入图片描述

总结

介绍了 快速失败 fail-fast 机制
它的原理和解决方案
在实际面试中
它和容器 通常一起问
重点掌握和理解



在这里插入图片描述

在这里插入图片描述

ConcurrentHashMap

使用线程安全的容器类
能保证我们的数据安全
消除并发修改的隐患



在这里插入图片描述

作为对应HashMap安全版本的
ConcurrentHashMap
它不仅是常用的容器之一
而且还经常出现在面试中



被问到最多的是
一: 同步
ConcurrentHashMap的实现原理是什么


在这里插入图片描述

ConcurrentHashMap的实现原理是什么
了解原理 我们要查看它的源码
它的源码非常复杂



在这里插入图片描述

ConcurrentHashMap
并没有简单粗暴地
直接给整个数组加锁
因为这样同一时刻
只能有一个线程操作数组
导致操作其他位置的线程只能等待
虽然数据是安全的
但是效率是极低的



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

所以设计者建议
给数组中每个头节点加锁
这样一来
并发度就从原来的 1
增长到 16
理论上 数组有多长 并发度就有多少
并发度上去了
效率也随之提高了



在这里插入图片描述

在并发数较大的环境下
不能再只用一个属性
来统计元素的个数
原因是
多个线程会同时读取属性的值
然后对其 ++
在同时赋给属性
这样一来就产生了线程安全问题
明明有多个线程对其 ++
结果确为 1




在这里插入图片描述

为了解决这个线程安全问题
我们给它加上锁
问题是解决了
但刚刚提升的效率
又给降下来了
获取锁 释放锁
耗时耗资源


在这里插入图片描述

在这里插入图片描述

重点!
于是 设计者采用
比锁轻量的CAS



在这里插入图片描述

Compare And Swap

Compare And Swap 的意思是 比较并交换


在这里插入图片描述

CAS 在多线程系列教程的出现过
使用CAS更新属性时
同一时刻 也只有一个线程能更新成功
其余线程更新失败



在这里插入图片描述

更新失败的线程再无限循环更新
直到更新成功
这看起来和锁没什么区别
效率也没高到哪里去



于是
设计者又提出 不妨新增一个数组
让更新失败的线程
先把值累加到
数组中去
在此之前
各个线程需要先知道
自己累加的位置(下标)



位置的计算方式
是和HashMap一样的
取一个随机数
当作是哈希值
然后&(数组长度-1)
这个操作
等同于 与数组长度取余



在这里插入图片描述

得到的余数
就是要累加的位置(下标)
累加失败的线程再循环累加
直到累加成功
最终元素个数
是由单个属性
和数组共同累计



在这里插入图片描述



从源码中我们也可以看到
元素最终个数
就是由
单个属性 baseCount
和CounterCell[]
共同累计来的



在这里插入图片描述

CounterCell[初始容量为2
扩容后的容量是原来的二倍



谈到扩容
ConcurrentHashMap和 HashMap是不同的



在这里插入图片描述

多线程协同扩容

ConcurrentHashMap 采用的是多线程协同扩容
怎么协同
从后往前
每个线程负责一段数据的迁移工作



在这里插入图片描述

一段有多长呢
这段代码
就是在计算一段有多长
n 是数组长度
stride 步长的意思
这里记录就是
每段的长度



NCPU 指你电脑上可用的核心线程数有多少
如果没超过 1 的话
那么步长 就为数组的长度
负责整个数组的数据迁移工作



如果超过 1 的话
那么就先让n 右移 3 位
也就是除以 8
然后再除以可用的核心线程数



在这里插入图片描述



例如 假设
数组长度 16
可用核心线程数 为 2
那么这里计算的步长就是
16/8/2=1



在这里插入图片描述

这个值小于最小步长数16

在这里插入图片描述

所以每个线程 至少要负责 16 个元素的迁移工作
迁移后的位置
会被填充ForwardingNode




在这里插入图片描述

ForwardingNode是一个哈希值为-1
key、value都为null的节点
其实它就是一个标识
表示该位置的节点已经迁移
或者正在迁移



在这里插入图片描述

在这里插入图片描述

key、value不能为null



ConcurrentHashMap的key和 value是都不能为null的
大致原因是:
当我们通过key获取到一个值为null的value时
你无法辨别 这到底是key 不存在呢
还是它所对应的值为null呢
有争议性



在这里插入图片描述

所以
设计者干脆就不让 key和value为null
这点 是与HashMap不同的




ConcurrentHashMap 与 HashMap的区别



第一: 线程是否安全方面
HashMap 是线程不安全的
ConcurrentHashMap是线程安全的


第二 : 扩容方面
HashMap 是单线程扩容
扩容过程中
如果其他线程 添加或删除集合中某个元素
还会引发并发修改异常


ConcurrentHashMap则没有此类问题
它是多线程协同扩容
安全又高效



第三 :统计元素个数方面
HashMap用一个属性来记录
ConcurrentHashMap则因为多线程的缘故
采用基础属性 baseCount 和 CounterCell 数组
累加计数




第四 : key value 能否为 null 方面
HashMap 能
ConcurrentHashMap不能 更为严谨一些




在这里插入图片描述

ConcurrentHashMap的常用方法


ConcurrentHashMap 继承自AbstractMap抽象类
实现了 Map接口
还实现了 ConcurrentMap 接口
ConcurrentMap继承自Map接口



在这里插入图片描述

ConcurrentHashMap 拥有Map 和ConcurrentMap里面的所有方法



但是在这些方法里面
它只有八个方法是常用的
其中
put和get 方法用得最频繁



在这里插入图片描述

总结(线程安全容器类)

本节介绍了 线程安全容器类

在这里插入图片描述

在实际开发中
它是用的最频繁的容器之一
多用于缓存数据
在面试中
也经常被问到


在这里插入图片描述

在这里插入图片描述

为什么ConcurrentHashMap只需给头节点加锁

为什么ConcurrentHashMap只需给头节点加锁
就可以保证线程安全了呢



在这里插入图片描述

换一种方式理解
不是给所有元素都加锁
这样不是更安全吗?



在这里插入图片描述

首先
ConcurrentHashMap只针对位置加锁
它不针对具体元素 加锁



在这里插入图片描述

头节点 之所以成为被锁对象
是因为头节点正好就在数组位置上
它下面的节点都是链在它的身后



另外
头节点不是固定不变的
例如 把"苹果" 删了
“香蕉” 就成为了头节点
此时 锁的就是 “香蕉”
足以证明
ConcurrentHashMap只针对位置加锁
不针对 具体元素 加锁
位置上是谁
就对谁加锁



在这里插入图片描述

另外 增删改查都是通过头节点进行的
例如 增加一个 “西瓜”
它先计算自己在数组中的位置(下标)
然后找到对应位置上的元素
即头节点
然后根据头节点的next 属性
依次往下走
找到合适的位置 插在尾部



在这里插入图片描述

删除 修改 查找
都是一样的道理
所有只要入口 每次只进一个线程
就可以保证数据的安全
因此
ConcurrentHashMap只需给头节点加锁就可以了



在这里插入图片描述

总结本节内容

本节解释了
ConcurrentHashMap
为什么只需给头节点 加锁?



在这里插入图片描述
这些内容有可能出现面试中
总结的Q&A 图


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值