MpscArrayQueue是一个固定大小的环形数组队列,继承自ConcurrentCircularArrayQueue
MpscArrayQueue的特点:
看一下MpscArrayQueue的属性(填充类除外)---
//生产者索引
private volatile long producerIndex;
//生产者边界
private volatile long producerLimit;
//消费者索引
private volatile long consumerIndex;
//继承自父类属性
//下标运算符
protected final long mask;
//消息存储数组
protected final E[] buffer;
首先,系统理解一下MpscArrayQueue的实现,MpscArrayQueue是一个循环数组队列,支持多生产者并发提交消息,重点依赖三个参数----pIndex,cIndex,producerLimit,看一下这三个参数在MpscArrayQueue中的作用,然后一步步深入他们是如何保证Mpsc运行机制的--
上图是普通情况下,cIndex,pIndex和producerlimit的关系,如图可以很容易得理解这三个参数的意义-
由上图我们大概可以理解MpscArrayQueue的工作机制:
当消费者对val1出队之后,此时producerlimit应该指向原来val1 的位置,也就是数组的头,从而保证生产者在数组满的时候可以从数组头的空闲位置生产消息;
上边主要从概念上解释了循环数组队列的生产消费实现,以及cIndex,pIndex,producerlimit这三个参数之间的逻辑关系,下边看一下MpscArrayQueue具体是怎么实现队列循环以及保证多生产者线程并发安全的--
先看一下三个参数的初始化----由于填充和继承的关系,略去了无关代码,只体现具体初始化的逻辑
然后看一下offer方法---
do
{//注意这里的do-While循环,循环体内主要获取并更新producerlimit,然后cas自旋获取pIndex锁,获取成功后再一次更新producerlimit;
//这也是官方代码设计巧妙的一点,正常逻辑在生产者提交消息之前首先抢占PIndex的索引即可,但是这里更新producerlimit保证了在抢占pindex前pindex是有效的
//获取producer index
pIndex = lvProducerIndex();
if (pIndex >= producerLimit)
{
//获取consumer index
final long cIndex = lvConsumerIndex();
//计算producer limit
producerLimit = cIndex + mask + 1;
if (pIndex >= producerLimit)
{
return false;
}
else
{
//将生产者限制更新为下一个指数,我们必须重新检查消费者指数这很激烈,但竞争是良性的
//这里的意思是---上边的if判断成运行到这里之后,可能有其他线程在此处并发修改了producerlimit,所以这里producerlimit的set是有竞争的,是线程不安全的,
// 但是这种不安全最终对消息的提交和生产不会造成并发错误---这里有一个非常巧妙的设计,下边解释里会详细解释
soProducerLimit(producerLimit);//这里对producerlimit的修改也是lazyset---底层调用了Unsafe类的putLongVolatile()方法
}
}
}
while (!casProducerIndex(pIndex, pIndex + 1));
这个循环可以理解为循环获取最新pIndex和producerlimit的值,然后比较二者的大小,最后cas抢占pIndex索引位置;当然producerlimit计算出新值之后要lazyset写回,这里有两点需要注意--
while(true){//死循环,自旋尝试抢占pIndex
pIndex = lvProducerIndex();
if (pIndex >= producerLimit)
{
final long cIndex = lvConsumerIndex();
producerLimit = cIndex + mask + 1;
if (pIndex >= producerLimit)
{
return false;
}
else
{
soProducerLimit(producerLimit);
}
}
if(casProducerIndex(pIndex, pIndex + 1))
break;
}
long producerLimit = lvProducerLimit();
while(true){//死循环,自旋尝试抢占pIndex
//注意这里pIndex必须在循环内更新,每次抢占失败说明pIndex被其他线程更新了,所以要重新获取pIndex;
pIndex = lvProducerIndex();
if (pIndex >= producerLimit)
{
return false;
}
if(casProducerIndex(pIndex, pIndex + 1))
break;
}
while(true){//死循环,自旋尝试抢占pIndex
//注意这里pIndex必须在循环内更新,每次抢占失败说明pIndex被其他线程更新了,所以要重新获取pIndex;
pIndex = lvProducerIndex();
//重新计算producerlimit的值
final long cIndex = lvConsumerIndex();
producerLimit = cIndex + mask + 1;
if (pIndex >= producerLimit)
{
return false;
}
soProducerLimit(producerLimit);
if(casProducerIndex(pIndex, pIndex + 1))
break;
}
final long cIndex = lvConsumerIndex();
producerLimit = cIndex + mask + 1;
可以看出来producerlimit的值依赖于mask和cIndex的值,而mask的值在队列初始化的时候指定并在运行过程中不会改变,唯一会改变的值是cIndex,这里我们认为volatile修饰的cIndex值的修改对A和B线程都是立即可见的,A和B会拿到真实的cIndex的值,另外假设A和B获取cIndex的中间cIndex的值发生了变化,最终导致A和B拿到的cIndex的值是递增的,最后计算出来的producerlimit的值当然也是递增的,这里假设A计算出来的producerlimit的值是10,B计算出来的producerlimit的值是11,再看一下producerlimit的写回逻辑--
soProducerLimit(producerLimit);
这里-底层调用了Unsafe类的putLongVolatile()方法,也就是lazyset写回(其实与lazyset没有关系,CPU的线程调度也会发生先后问题,因为本身没有做同步机制),所以理论上producerlimit在AB都更新完成之后应该变为11,但是实际上并不能保证这一点,有可能A在B之前获取到的cIndex的值,并完成producerlimit的计算,但是在写回之前CPU调度A写回之前先执行了线程B,先将11写回了producerlimit,之后才调度线程A将10写回了producerlimit,这样导致producerlimit的最终值变成了10而不是11,这确实在MpscArrayQueue的实现中是有可能发生的,但是这里为什么官方说这种竞争是良性的呢?其实读到这里就已经发现了----每次pIndex的值小于producerlimit的时候重新计算producerlimit的值,这时候只要保证pIndex的值是递增的就不会影响到生产者消息的提交,只是多了一次producerlimit的重新计算而已;这里的设计也体现了cas同步的一些基本思路;而对于pIndex的值使用了cas操作,不会导致多个线程获取到同一个pIndex的情况---如果多个线程获取到同一个pIndex的时候,才会真正产生多线程并发问题;
至此,就分析完了MpscArrayQueue中对于多生产这线程并发提交的实现的分析,其实总结起来就是一句话---通过cas抢占pIndex来完成多线程并发提交的问题;而producerlimit主要是为了实现环形数组的实现,环形数主要通过对数组长度取模来计算的--实际实现是对length-1按位与来代替取模计算;
环形数组的实现依赖mask;首先看一下mask的定义;
int actualCapacity = Pow2.roundToPowerOfTwo(capacity);
mask = actualCapacity - 1;
首先将容量近似为定义的参数的下一个2的n次幂的值,比如我们传入的值为3,则队列实际长度为4,这样与capacity-1计算出来的mask进行&运算相当于取模;
整体看一下相关代码及注释---
public boolean offer(final E e)
{
if (null == e)
{
throw new NullPointerException();
}
//官方注释--- 使用consumer index的缓存视图可能在循环中更新---
// 这句话怎么理解--producer limit计算方式--
// producerLimit = cIndex + mask + 1
//可以看出producer limit的值跟mask和consumer limit有关,
//首先看一下mask的值--
// int actualCapacity = Pow2.roundToPowerOfTwo(capacity);
// mask = actualCapacity - 1;
//可以看出mask的值是跟capacity有关,而容量是我们初始化队列的时候就定义好的值,所以运行过程中producerlimit的值的变化取决于consumer index,
//而这里获取到producerlimit的值之后才进入cas自旋更新pIndex,所以在自旋的过程中consumerindex的值可能会发生改变,导致producerlimit的值发生改变
//所以这里在自旋开始前获取到的producer limit的值是源于cIndex的一个缓存值--在自旋成功后该值未必是正确的--这也导致了自旋成功后第二次判断producerlimit的值;
//其实这里可以先不获取producerlimit的值,自旋跟新pIndex的值之后再获取,而官方这里提前获取可能是为了优化性能---断定这里cIndex被修改的可能性小(因为是单消费者)
final long mask = this.mask;
long producerLimit = lvProducerLimit();
long pIndex;
do
{//注意这里的do-While循环,循环体内主要获取并更新producerlimit,然后cas自旋获取pIndex锁,获取成功后再一次更新producerlimit;
//这也是官方代码设计巧妙的一点,正常逻辑在生产者提交消息之前首先抢占PIndex的索引即可,但是这里更新producerlimit保证了在抢占pindex前pindex是有效的
//获取producer index
pIndex = lvProducerIndex();
if (pIndex >= producerLimit)
{
//获取consumer index
final long cIndex = lvConsumerIndex();
//计算producer limit
producerLimit = cIndex + mask + 1;
if (pIndex >= producerLimit)
{
return false;
}
else
{
//将生产者限制更新为下一个指数,我们必须重新检查消费者指数这很激烈,但竞争是良性的
//这里的意思是---上边的if判断成运行到这里之后,可能有其他线程在此处并发修改了producerlimit,所以这里producerlimit的set是有竞争的,是线程不安全的,
// 但是这种不安全最终对消息的提交和生产不会造成并发错误---这里有一个非常巧妙的设计,下边解释里会详细解释
soProducerLimit(producerLimit);//这里对producerlimit的修改也是lazyset---底层调用了Unsafe类的putLongVolatile()方法
}
}
}
while (!casProducerIndex(pIndex, pIndex + 1));
/*注意:新的pindex在数组中的元素之前可见。如果我们依赖poll()的索引可见性,我们将需要处理元素不可见的情况。
* ---这句官方注释怎么理解:
* 多生产者为了实现生产者并发生产消息,每个生产者在抢占到Pindex之后会先将pindex暴露出去,提供其他的生产者抢占,之后才对具体的消息进行lazyset,这里就有一个问题了,消费者也能看到这个pIndex,所以消费者想
* 消费这个pIndex对应的消息的时候有可能这个时候生产者还未实际进行写入,或者写入不可见,所以在消费者poll的时候要处理这中情况
* */
// Won CAS, move on to storing
final long offset = calcCircularRefElementOffset(pIndex, mask);
// REF_ARRAY_BASE + ((index & mask) << REF_ELEMENT_SHIFT);
//REF_ARRAY_BASE表示数组初始位置,相当于0,REF_ELEMENT_SHIFT为2,这里左移两位相当于×4
//重点看一下index&mask, index 表示当前生产者线程获取到的index,mask的计算方式--
// int actualCapacity = Pow2.roundToPowerOfTwo(capacity)---从中可以找到下一个二次幂的值。
//返回下一个2的正幂,如果是2的幂,则返回该值。负值映射到1。
// mask = actualCapacity - 1;
soRefElement(buffer, offset, e);
return true; // AWESOME :)
}