目录标题
- LinkedHashMap
- Map集合框架结构体系图
- 什么是“添加顺序”
- 什么是访问顺序
- LinkedHashMap常用方法
- LinkedHashMap总结
- TreeMap集合
- Map集合框架结构体系图
- 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
- TreeMap的特点 :key唯一
- TreeMap的常用方法
- 总结
- HashSet集合
- Collection集合框架结构体系图
- 练习
- HashSet 的常用方法
- HashSet 与 HashMap 的区别
- 总结:
- LinkedHashSet
- Collection集合框架结构体系图
- Linked "链式"的意思 HashSet "哈希集合"的意思 LinkedHashSet :链式哈希集合 所以可以看出 它扩展自 HashSet 但它内部使用的是LinkedHashMap LinkedHashMap是有序的
- 所以 LinkedHashSet 也是有序的 关于LinkedHashMap更多内容 在之前学过 就不介绍了 当我们使用LinkedHashSet存数据的时候 只需要把 key 当 value 使 value再用一个统一的值填充即可 这个值就定义在HashSet的内部
- 元素取出的顺序 从头节点开始 依次是 苹果 香蕉 梨子 葡萄 柠檬
- 练习
- 程序验证: 从执行结果来看 和我们刚刚说的顺序一样 由此 我们可以总结出 LinkedHashSet的第一个特点 有序
- 重点! 元素存入的顺序 和 取出的顺序 一致 其他的特点 及优缺点 都和 LinkedHashMap一样
- LinkedHashSet 常用方法
- LinkedHashSet 与 LinkedHashMap的区别
- 总结:
- TreeSet
- Collection集合框架结构体系图
- Tree "树"的意思 这里的树 是指"二叉树" Set :"集合"的意思 TreeSet: 二叉树集合 的意思
- 它内部使用的是TreeMap TreeMap是基于红黑树实现的 红黑树拥有自平衡的特点 它会对元素进行排序 默认的排序规则是自然排序 自然排序是通过元素的compareTo方法完成的 自定义排序则需要实现Comparator接口 遍历元素的方式 : 采取的是 中序遍历 先左节点 再根节点 最后右节点
- 练习
- 程序验证: 从执行结果来看 和我们刚刚说的顺序一样 由此 我们可以总结出 TreeSet的第一个特点: 无序
- 元素存入的顺序和取出的顺序不一致 其他的特点以及优缺点 都和TreeMap 一样 关于TreeMap 的学习内容在之前讲解过
- TreeSet 常用方法
- TreeSet 结构体系图
- 比较HashSet和LinkedHashSet
- 总结 TreeSet
- 快速失败(fail-fast)机制
- 在集合中有一种保护机制 叫 fail-fast 翻译过来就是 fail-fast : 快速失败 它能够防止多个线程并发修改同一个容器的内容 如果发生了并发修改的情况 那么就会触发“快速失败”的机制
- 也就是抛出并发修改异常
- 例如 你在迭代遍历某个容器的过程中 另一个线程介入其中 并且插入或者删除此容器内的某个元素 那么就会出现问题 不光是多线程会出现问题 就连单线程也会出现问题
- 重点! 下面 通过一个过程 来复现以下问题是怎样产生的 试想一下 当我们的容器正在扩容时 突然有两个元素被添加进来 请问 : 它们该放在哪里
- 放在旧容器不合适 再者说 旧容器也容不下它们 只能放在新容器 2 号位 是梨子的 3 号位 是葡萄的 所以它们只能往后顺位 由于它们是同一时刻被添加进来的 导致它们计算的位置(下标)是一样的 这就产生了 同一个位置出现了两个元素的情况
- 重点! 问题也就显示了出来 为了防止这样的问题出现 Java容器类采用了快速失败(fail-fast)机制 它会监视容器的变化 具体的做法是 在容器类中 定义一个modCount 属性 用于记录 容器修改 次数 初始值为 0
- 只要容器有添加或删除元素的操作 modCount就 ++
- 在每次获取迭代器的时候 迭代器会先读取一次modCount的值 迭代的时候进行比较
- 如果两个值不相等 那么就说明有其他线程修改了容器 导致数据混乱 因此抛出并发修改异常 以上就是“快速失败(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 与 HashMap的区别
- 第一: 线程是否安全方面 HashMap 是线程不安全的 ConcurrentHashMap是线程安全的
- 第二 : 扩容方面 HashMap 是单线程扩容 扩容过程中 如果其他线程 添加或删除集合中某个元素 还会引发并发修改异常
- ConcurrentHashMap则没有此类问题 它是多线程协同扩容 安全又高效
- 第三 :统计元素个数方面 HashMap用一个属性来记录 ConcurrentHashMap则因为多线程的缘故 采用基础属性 baseCount 和 CounterCell 数组 累加计数
- 第四 : key value 能否为 null 方面 HashMap 能 ConcurrentHashMap不能 更为严谨一些
- ConcurrentHashMap的常用方法
- ConcurrentHashMap 拥有Map 和ConcurrentMap里面的所有方法
- 总结(线程安全容器类)
- 为什么ConcurrentHashMap只需给头节点加锁
- 为什么ConcurrentHashMap只需给头节点加锁 就可以保证线程安全了呢
- 换一种方式理解 不是给所有元素都加锁 这样不是更安全吗?
- 首先 ConcurrentHashMap只针对位置加锁 它不针对具体元素 加锁
- 头节点 之所以成为被锁对象 是因为头节点正好就在数组位置上 它下面的节点都是链在它的身后
- 另外 头节点不是固定不变的 例如 把"苹果" 删了 "香蕉" 就成为了头节点 此时 锁的就是 "香蕉" 足以证明 ConcurrentHashMap只针对位置加锁 不针对 具体元素 加锁 位置上是谁 就对谁加锁
- 另外 增删改查都是通过头节点进行的 例如 增加一个 "西瓜" 它先计算自己在数组中的位置(下标) 然后找到对应位置上的元素 即头节点 然后根据头节点的next 属性 依次往下走 找到合适的位置 插在尾部
- 删除 修改 查找 都是一样的道理 所有只要入口 每次只进一个线程 就可以保证数据的安全 因此 ConcurrentHashMap只需给头节点加锁就可以了
- 总结本节内容
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
并没有简单粗暴地
直接给整个数组加锁
因为这样同一时刻
只能有一个线程操作数组
导致操作其他位置的线程只能等待
虽然数据是安全的
但是效率是极低的