并发量多时springboot一直报空指针_图解算法:队列、并发队列

提到队列,我们会在很多地方听到或者看到,

那我们来看一下这位不太说话的老朋友,

从栈很容易联想到队列的实现

  • 栈是先进后出的数据结构,队列而言它是先进先出。
  • 对栈而言,在栈顶有一个指针即可。
  • 队列是需要两个指针,一个在队头,一个在队尾。对应着入队操作和出队操作。
  • 基于数组实现的是顺序队列,基于链表实现的是链式队列。

一个数组实现的顺序队列,在 入队了 AA 、BB 、CC 后,

队头指针 head=0,队尾指针 tail=3。如下图:

3ddb87f6f270a262aaacec6db6c5e450.png

紧接着,又有两次出队,同样,对于出队head指针往后移动两个:

5d39d114e79b76c1dbae6b650a098347.png

以上两个图对应的入队出队操作,也是很容易看出问题所在:

随着入队出队一波操作,tail指针很容易移动到最后的位置,表面上不能再入队了。

但是极有可能如图二一样,头指针head前面有大片空地。

怎么办?搬!我在出队之后,后面的数据往前挪,我们可以称之为移动补位。

但是每一次出队操作都去搬数据,时间复杂度想想就会很高 O(n)

怎么优化?

tail指针抵达末尾,同时head指针不在队头。也就是tail到了最后,且head前面有空。

此时触发数据搬移,过程如下:

efef3f4addbf034012cb220d2fc7df3d.png

人的思想不断进步,并且思考如何做得更加轻巧灵活。

我们会思考,可不可以不用搬移数据呢?

可以,接下来轮到循环队列登场了。。。。。。

循环队列,顾名思义。首尾相连形成环。哝,就是这个样子:

bc509d095a4661b8a84ac3cf12ad3f9b.png

长得这么好看,一定要对得起我们对它的期望。

经过一番出队入队,头部索引=2,尾部指针指向最后一个位置,即将接受FF入队,

bfee8b0d1fb1628bbc3b8d6e436d57a9.png

此时看上去又到了挪动数组的时候了?

环形的存在就是为了避免队列的数据搬移,我想你已经想到了它的灵巧之处。

对,就是将数据FF填充到索引=5处,tail指针移动到下一个,也就是索引=0处,就成了这样:

150595fb00078a6a4117b149dcbc7833.png

队列在平时工作时用的机会场景比较少,但是在一些偏底层系统中确实应用比较广泛。

比如:阻塞队列、并发队列

阻塞队列,就是在队空时,取数据会被直接拒绝。直到有数据才会允许被访问。

这种模型类似于 生产-消费关系,对的,这也是很多的消息队列的思想和应用。

这种阻塞队列可以协调生产和消费的关系。当然,也可以生产的i消息被多个消费。

这又产生了一个线程并发问题,我们如何保证线程安全呢?这就需要并发队列。

基于数组的循环队列+CAS原子操作,可以很好的实现无锁并发队列。

基于以上,微软给我们所提供的这些源码:

  • 队列 Queue ;
  • 泛型队列 Queue;
  • 阻塞泛型集合 BlockingCollection
  • 以及微软强大的并行库中的并发泛型队列 ConcurrentQueue

我们着重看一下泛型队列和并发泛型队列

队列 Queue 、泛型队列 Queue

我们直接看一下泛型版本的:

0、注释说明:这是一个基于数组实现的环形队列,也就是循环队列

dc1542dae31f3c1a033e1663a0412d71.png

1、初始定义

058240fc62a1ef49455dd1068da53cf3.png

2、重要的私有变量

c1c59554c0065c18bfd3796c6bdec194.png

3、入队:分为两块主逻辑,一个是队满,一个是正常插入。

cecf0df07f45decd73e5934ad9bf1832.png

第0步已经注释说明这是一个循环队列,所以我们借此机会分析一下这个循环队列。

  • 队满 1if (_size == _array.Length) 2倍扩容并且有最小装载量判断。
  • 正常
   _tail = (_tail + 1) % _array.Length; 下面我们来看看这句话怎么来的。

对于非循环队列,头尾指针和数组的关系好确认。

而循环队列,因为是一个环,所以怎样定位移动后的指针位置才是关键的。

150595fb00078a6a4117b149dcbc7833.png

数组长度=6

当我入队FF,原来尾部指针=5,当前尾部指针=0;

接着入队GG, 原来尾部指针=0,当前尾部指针=1;

当我入队HH,原来尾部指针=1,当前尾部指针=2;

规律:当前指针 = (原来指针 +1) % 数组长度

4、出队同3

ConcurrentQueue

注释说的很明白,这是一个无锁并发队列

我们在看源码之前先来了解一些定义

对于现在的多CPU、以及超线程概念的操作系统来说,CPU和内存之前存在处理速度上的差距,所以中间加了寄存器和高速缓存来缓冲。

多线程并发情况下,多核计算机,一个CPU读取的是在寄存器中的值,另一个CPU读取的是内存中的值,这就造成了数据不同步。

对于产生的并发问题,我们来看看并发队列对这些的处理。

我们先来理解接下代码中涉及到的名词:

1、易失结构 volatile : 告诉编译器和CLR不需要优化代码顺序,使得代码可控。不用将字段缓存到寄存器,缓存在内存中就行。

2、互锁结构 Interlocked : CAS保证原子性读取操作

3、自旋锁 :原地打转,直到达到条件才离开。对于线程来讲,一直持有资源不撒手。

4、线程类提供了几个方法:

  • Thread.Sleep(0):挂起自身,让出剩余的时间片,强迫系统调度其他同级或者更高级的线程。
  • Thread.Sleep(1):强迫进行一次上下文切换
  • Thread.Ylied():提前结束剩余的时间片,使得同级或者低级线程可能被调度。
  • Thread.SpinWait():超线程CPU模式下,强迫自身暂停,允许CPU调度其他线程。

5、CAS理论:compare and swap 比较并交换。该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。

天也不早了,人也不少了,让我们干点正事。简单看看入队和出队操作。

入队:

需求是怎样保证入队的原子性?

通过 Interlocked 声明同步块,只允许一个线程抢占资源进行入队,其他线程使用自旋锁进行原地等待。

等当前线程释放同步块,其他线程再次抢占同步块,然后入队。直到队满跳出。

  • 下面这是声明了自旋锁,线程进行入队抢占。
29e1d92da581527e234bf3190c68f2ef.png
  • m_high =-1
ab590d40a6dac51b77c8ea61f63ac445.png
  • m_high 通过 Interlicked CAS原子操作,递增。进行入队或者队满判断。
c08f030d8cd6125c61150fccf2a854b5.png

出队:也是类似,通过自旋锁,抢占同步块进行原子性出队操作。

最后我们再来悄悄看看 自旋锁自旋逻辑:

f1b650d02dae70f5dcfb30078c4e7996.png

自旋至少10次,然后进行相应的自旋等待,并且相应的让出自己的时间片,让其他低级别线程可以得到调度。

a93bbc2d83f66b617e6dadb4b6103407.png

总体来说,并发队列通过CAS进行原子性入队和出队,并结合自旋锁进行抢占资源。

也就是很多的线程并发入队或者出队,同一时刻只有一个可以进行原子性入队出队。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值