并发编程-集合篇

1、线程安全的集合(单列)怎么选择?

**List、Set集合,线程安全的有哪些?**

* Vector,有,但是基本不考虑。(synchronized同步方法)
* Collections.synchronizedList,也可以拿到线程安全的集合(synchronized同步代码块)
* CopyOnWrite系列。(lock锁)

**如果数据体量贼大,不考虑读的问题,还需要保证线程安全?**

答:

* 第一点,不能考虑CopyOnWrite。数据体量贼大,如果用CopyOnWrite会导致空间占用太多。
* 第二点,如果不在业务代码上实现锁的话,基本职能选择Collections.synchronizedList。
* 第三点,不采用JDK提供的线程安全的集合,自己通过代码,针对读写操作最针对性的加锁。
  * 可以在业务带上中,去完成读写锁的操作
  * 也可以仿照Collections.synchronizedList,直接做一个装饰者模式,套一层。

**至于多列集合,不需要考虑了,上Concurrent系列。**

2、CopyOnWrite集合需要注意的点?

CopyOnWrite在做写操作的时候,会复制一套副本,在副本中做各种操作,这样不会影响读线程去读取原生数据的。基于这种方式,读操作是不需要加锁的,不会出现线程安全问题。

毕竟CopyOnWrite需要复制一套副本,如果数据体量比较大的时候,不推荐使用了,浪费空间。

**我之前真是操作过的:** WebSocket里面,存储客户端的Session,Session需要存储,同时可能会客户端并发连接,而且Session体量不会太大。当时选择的CopyOnWriteArraySet。

3、ConcurrentHashMap存储数据结构是什么样子呢?

问题基础,但是必须要会:数组 + 链表 + 红黑树

JDK1.8中,才出现红黑树结构。

正常1.7就是数组 + 链表。在没有hash冲突的情况,数据就仍在数组上,当数据在数组上时,查询效率很高,是O1

但是毕竟无法规避Hash冲突问题,当存在Hash冲突时,数据会被挂在数组的某一个索引下,形成一个链表。这样查询的效率会变低,时间复杂度是On

所以红黑树就是在解决链表过长的问题,如果链表过长,会导致查询的效率降低。此时如果链表过程,就可以生成一套红黑树,平衡二叉树,查询效率比链表快,时间复杂度是Ologn。

**为什么选择红黑树提升查询效率,别的数据结构可以嘛?**

跳表可以嘛?换是可以的。但是,ConcurrentHashMap默认不支持修改结构。

跳表空间占用率更高。写效率不想红黑树那么复杂。

4、ConcurrentHashMap的负载因子可以重新指定吗?

负载因子就是这东西。不能改!

而且在ConcurrentHashMap的有参构造中,虽然可以穿度一个负载因子的参数,但是无法修改他,在有参构造的逻辑里,仅仅是拿着传入的loadFactor计算初始数组的长度。没有给核心的loadFactor做修改。

同时HashMap是允许修改的。

而且在ConcurrentHashMap中,没有基于loadFactor计算阈值,而是直接基于位运算计算的,结果其实和×0.75一模一样。没有直接×,就是因为位运算更快。

**如果是HashMap,修改了负载因子,不用0.75,会有什么问题么?**

如果设置的比较小,会造成频繁的扩容,比如设置0.5,16长度的数组,元素有8个就扩容了。

如果设置的比较大,会造成大量的Hash冲突,比如设置为1,16长度的元素,元素个数达到16个才会扩容。大量的hash冲突会造成数据挂到链表甚至生成红黑树,查询效率会降低。

是可以改的,只不过对泊松分布计算出来的概率,有一定影响。

5、ConcurrentHashMap的散列算法?相比HashMap有什么区别?

ConcurrentHashMap还是HashMap,都是对key进行一个hashCode方法,然后配合数组长度做一个&运算,得到当前元素要存储的位置。

hashCode返回的值是一个int类型,站32个bit位。

同时数组的长度,如果要占用16个bit位,长度是65535。

所以一般计算时,hashCode的高16个bit位,无法参与到运算中。导致很多hashCode高位不一样,但是低位一样的key,出现了Hash冲突,存储到了一个位置,形成了链表。

所以无论是ConcurrentHashMap还是HashMap,都是先让hashCode的 **高低位先做^运算** ,然后再去和数组长度 - 1做&运算,得到最终存储的位置。

**为什么数组长度-1做计算?**

因为长度为16的数组,索引位置是0~15。

如果不-1,会造成与数组做&运算,只有0或者16这两个值。首先会大量冲突,另外16不在索引范围内。

6、ConcurrentHashMap 中 sizeCtl 字段的作用?

sizeCtl是用于初始化数组和做扩容是的一个控制字段。

不一样的值,代表不同的意思:

* sizeCtl == -1:代表ConcurrentHashMap正在初始化数组。
* sizeCtl < -1:代表ConcurrentHashMap正在初始化扩容
* sizeCtl == 0:代表默认状态,啥事没有。
* sizeCtl > 0:有可能代表两个意思:
  * 数组没初始化时:可能代表ConcurrentHashMap初始化数组时的长度。
  * 数组已经初始化:sizeCtl的值代表下次扩容的阈值。

 7、ConcurrentHashMap扩容的整体流程?

* 计算扩容标识戳。
* 开始扩容
  * 第一个执行扩容的线程

    * 给sizeCtl赋值,表示当前线程要扩容了。
    * 计算步长(每次迁移多少个索引位置的数据到新数组)
    * 初始化新数组,长度是老数组的二倍。
    * 领取步长数索引个数的迁移数据的任务
    * 将老数组数据迁移到新数组。
    * 老数组数据迁移完了,扔个ForwardingNode。
    * 4,5,6循环操作,直到数据全部迁移完
  * 后续执行扩容的线程

    * 给sizeCtl + 1,代表来了一个线程帮忙做迁移数据操作
    * 计算步长(和第一个线程计算的结果一致)
    * 领取步长数索引个数的迁移数据的任务
    * 将老数组数据迁移到新数组。
    * 老数组数据迁移完了,扔个ForwardingNode
    * 循环3,4,5操作,直到数据全部迁移完毕

8、ConcurrentHashMap扩容时的扩容标识戳干嘛的?

扩容标识戳,有个特点,是int类型,第17个bit位一定是1。并且扩容标识戳是基于oldTable的长度计算出来的。

然后需要将扩容标识戳左移16位

* 如果是第一个扩容的线程,对低位 + 2,代表第一个扩容的线程来了。
* 如果是其他协助扩容的线程,对低位 + 1,代表我来帮忙了。

然后赋值给sizeCtl,第17个bit位一定是1可以确保是负数。

**为啥基于oldTable的长度计算?**

为了确保协助扩容的线程,一定是和正在扩容的线程预期的长度一致。

比如,现在正在做32~64的扩容。此时一个线程想帮忙,但是这个线程希望从64-128。那这个想帮忙的线程,就没法帮忙。

9、ConcurrentHashMap如何统计元素个数?

首先ConcurrentHashMap是线程安全的集合,统计元素个数,肯定要确保线程安全。

计算元素个数,无非是++,--。

确保++,--线程安全,还要有效率。

这里ConcurrentHashMap选择的就是LongAdder,首先基于CAS对元素做++,--操作,确保线程安全。并且LongAdder除了ConcurrentHashMap记录元素个数的baseCount外,同时也准备了一个CounterCell数组,每一个CounterCell里都有一个value记录元素个数。这样CAS就可以针对多个位置执行,以尽量减少CAS的空转情况。

10、ConcurrentHashMap在JDK1.7和1.8如何保证写数据线程安全?

JDK1.7里的锁,一般称为Segement,是基于ReentrantLock实现的。

JDK1.8里,用了两种锁,如果数据要写入到数组上,基于CAS的方式尝试写入。如果数据要挂到链表或者红黑树上时,采用synchronized锁住数组上的Node。

11、ConcurrentHashMap在JDK1.8中存在的BUG?

在扩容的地方,有协助扩容的判断,在这个判断中,中间两个个判断都是毫无意义的。

```java
// 第一个判断,是为了确保协助扩容的线程,和正在扩容的线程的长度是一致的。
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || 
    // 正常这么写:sc == rs << RESIZE_STAMP_SHIFT + 1 ,目的是为了判断,当前扩容是否已经到了最后的检查阶段。BUG ~~~
    sc == rs + 1 || 
    // 正常这么写:  sc == rs << RESIZE_STAMP_SHIFT  + MAX_RESIZERS,目的是为了判断,当前过来协助扩容的线程,是否已经到了最大值。  BUG~~~
    sc == rs + MAX_RESIZERS || 
    // 后面这俩不是BUG~
    (nt = nextTable) == null ||
    transferIndex <= 0)
    break;
```

在协助扩容前,有几个判断,主要是判断扩容是否结束,以及协助扩容的线程是否已经达到最大值的这两个判断,这两个判断没有将扩容标识戳做左移操作,就直接与sizeCtl做判断了,这种判断是没有任何意义的。

12、ConcurrentHashMap在扩容期间可以查询数据嘛?

写操作才会帮助扩容,读操作不会协助扩容。

而且不会等待扩容结束再查,而是直接查询。

查询时,直接查询老数组索引位置,如果查询到了数据,直接返回。

如果发现老数组的索引位置上,放了一个ForwardingNode,代表数据已经被迁移到了新数组。直接去新数组找数据。

(ConcurrentHashMap中的读操作,是不会阻塞的,什么情况都不阻塞。)

13、ConcurrentHashMap的红黑树中为何会保留一套双向链表?

ConcurrentHashMap在转换红黑树的时候,会将Node更改为TreeNode。TreeNode是继承自Node的。TreeNode中不但包含了红黑树的parent,left,right,red之外,还有维护的prev,以及继承自父类的next。所以ConcurrentHashMap中,不但包含红黑树,还包含一套完整的双向链表。

为什么保留双向链表?

1、红黑树结构在执行写操作的时候,为了保证平衡,会做左旋或者右旋操作。做旋转操作时,指针是会变化的,如果有读线程在红黑树里检索,同时写现在来写入数据,可能会导致读操作无法读取到具体内容。所以在有写线程写入红黑树时,读线程可以直接读取这个双向链表。

2、如果在迁移数据时,某一个索引位置下存放的是红黑树,怎么迁移啊?是不是有点麻烦啊?所以在迁移数据时,可以不通过这种麻烦的红黑树做迁移,而是直接利用双向链表做迁移操作。同理,在红黑树退化为链表时,也没那么麻烦,直接操作双向链表即可。

14、lock与sync区别

单词不一样

lock需要new着用,sync关键字,只能同步代码块和同步方法用

lock需要手动释放,sync不需要手动释放

lock基于AQS实现,sync基于对象实现,重量级锁采用ObjectMonitor

lock支持公平和非公平锁,sync只是非公平锁

lock基于Condition的await和signal做挂起和唤醒,sync通过wait和notify做这个操作

  • 15
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

令人着迷

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

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

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

打赏作者

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

抵扣说明:

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

余额充值