纯CAS为啥比加锁要快?

同样是修改数据,一个采用加锁的方式保证原子性,一个采用CAS的方式保证原子性。

都是能够达到目的的,但是常用的锁(例如显式的Lock和隐式的synchonized),都会把获取不到锁的线程挂起,相对于CAS的不挂起,多了挂起和唤醒的开销。

题外话:CAS与锁的关系

CAS只是在这个场景下,比使用锁来得更纯粹,因为只做数据更新,所以开销更少。但是业务上为了保证一系列操作的原子性,还是要使用锁的。而且锁的底层实现,也依赖于类似于CAS这样的原子性操作。

尾指针是如何管理的,如何防止覆盖旧数据?

别的帖子都说RingBuffer中不维护尾指针,尾指针由消费者维护(所谓维护指针,就是修改、移动指针)其实这一句话有点误导性,如果RingBuffer不知道尾部在哪里,那它的数据存储肯定就会出问题,例如把还没消费过的数据给覆盖了。

确实,消费者会自行维护自己的消费指针(消费者指针是消费者消费过的最后一条数据的序号,下一篇中会详细讲到),RingBuffer也不会去干涉消费者指针的维护,但是它会引用所有消费者的指针,读取他们的值,以此作为“尾部”的判断依据。实际上就是最慢的那个消费者作为边界

我们直接来看代码,这个是RingBuffer的publishEvent方法,我们看到,它首先取得一个可用的序列号,然后再将数据放入该序列号的对应位置中。

@Override
public void publishEvent(EventTranslator<E> translator)
{
//1.先通过原子操作,得到一个可用的序号
final long sequence = sequencer.next();
//2.将该序号对应位置的元素进行转换,接着发布
translateAndPublish(translator, sequence);
}

我们来看看这个序列号是如何取得的。我们先看Sequencer的SingleProducerSequencer实现。这里就是判断如果生产者新指针的位置是否会超过尾部,如果超过尾部就挂起片刻,后续再尝试(生产者的等待方式是固定的,不像消费者有一个等待策略)

这里附上几个图可能更好理解:(右边是后续补充的用“画图”画的,对单元格添加一些颜色进行区分)

情况1:队列已满,生产者尝试使用新序号14,但由于(14 - 8 = 6),由于最慢的消费者目前消费的最后一条数据的序号是5,5号之后的数据还没被消费,6 > 5,所以序号14还不能用。生产者线程挂起,下次再次尝试。

情况2:消费者1消费了序号6的数据。(14 - 8 = 6) 不大于 6,这时序号14可用,生产者得到可用的序号。

    @Override
public long next()
{
return next(1);
} /**
* @see Sequencer#next(int)
*/
@Override
public long next(int n)
{
if (n < 1 || n > bufferSize)
{
throw new IllegalArgumentException("n must be > 0 and < bufferSize");
} long nextValue = this.nextValue; //当前RingBuffer的游标,即生产者的位置指针 long nextSequence = nextValue + n;
long wrapPoint = nextSequence - bufferSize; //减掉一圈
long cachedGatingSequence = this.cachedValue; //上一次缓存的最小的消费者指针 //条件1:生产者指针的位置超过当前消费最小的指针
//条件2:为特殊情况,这里先不考虑,详见:
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
{
cursor.setVolatile(nextValue); // StoreLoad fence long minSequence;
//再次遍历所有消费者的指针,确认是否超过
//如果超过,则等待
while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
{
LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
} this.cachedValue = minSequence;
} this.nextValue = nextSequence; return nextSequence;
}

另外对于多生产者的情况,在不会越界的情况下,需要通过CAS来保证获取序号的原子性。具体可以查看MultiProducerSequencer的next方法。

消费者指针是如何读取的?

RingBuffer如何知道有哪些消费者?哪些gatingSequense是从哪里来的?

在构建RingBuffer注册处理类的时候,就将消费者Sequense注册到RingBuffer中了。

看代码的话,定位到gatingSequences在AbastractSequencer,对应的有个addGatingSequenses方法用于注入gatingSequence

public abstract class AbstractSequencer implements Sequencer {
//...
protected volatile Sequence[] gatingSequences = new Sequence[0]; @Override
public final void addGatingSequences(Sequence... gatingSequences)
{
SequenceGroups.addSequences(this, SEQUENCE_UPDATER, this, gatingSequences);
} //...
}

再查看addGatingSequences被调用的地方,即通过RingBuffer的方法,设置到Sequencer中,这个Sequencer是生产者使用的序号管理器

public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E> {
//...
protected final Sequencer sequencer; public void addGatingSequences(Sequence... gatingSequences) {
sequencer.addGatingSequences(gatingSequences);
}
//...
}

而RingBuffer的addGatingSequence则在Disruptor配置处理器的时候被调用

public class Disruptor<T> {
//...
private final RingBuffer<T> ringBuffer;
private final ConsumerRepository<T> consumerRepository = new ConsumerRepository<>(); public EventHandlerGroup<T> handleEventsWith(final EventProcessor... processors)
{
for (final EventProcessor processor : processors)
{
consumerRepository.add(processor);
} final Sequence[] sequences = new Sequence[processors.length];
for (int i = 0; i < processors.length; i++)
{
sequences[i] = processors[i].getSequence();
} ringBuffer.addGatingSequences(sequences); return new EventHandlerGroup<>(this, consumerRepository, Util.getSequencesFor(processors));
}
//...
}

缓存的意义是什么?

我们看到在SiingleProducerSequencer的next方法中,会缓存上一次的消费者最小序列号,这有什么用呢?

用途就是不需要每次都读取各消费者的序号,只要没超过上一次的最小值的地方都可以直接分配,如果超过了,则进行再次判断

为啥读取最小值不需要保证原子性?

看了这个获取最小消费序号的,可能会奇怪,为啥这个操作不需要上锁,这个不是会获取到旧值吗?

确实,这个最小值获取到的时候,实际上数值已经变更。但是由于我们的目的是为了防止指针越位,所以用旧值是没有问题的。(旧值<=实际上的最小值)

public static long getMinimumSequence(final Sequence[] sequences, long minimum)
{
for (int i = 0, n = sequences.length; i < n; i++)
{
long value = sequences[i].get();
minimum = Math.min(minimum, value);
} return minimum;
}

【杂谈】Disruptor——RingBuffer问题整理(一)的更多相关文章

  1. Disruptor Ringbuffer

    系列译文: http://ifeve.com/disruptor/ 当有多个消费者时,(按Disruptor的设计)每个消费者各自控制自己的指针,依次读取每个Slot(也就是每个消费者都会读取到所有的 ...

  2. 架构师养成记--16.disruptor并发框架中RingBuffer的使用

    很多时候我们只需要消息中间件这样的功能,那么直需要RinBuffer就可以了. 入口: import java.util.concurrent.Callable; import java.util.c ...

  3. disruptor笔记之八:知识点补充(终篇)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  4. 架构师养成记--15.Disruptor并发框架

    一.概述 disruptor对于处理并发任务很擅长,曾有人测过,一个线程里1s内可以处理六百万个订单,性能相当感人. 这个框架的结构大概是:数据生产端 --> 缓存 --> 消费端 缓存中 ...

  5. 构建高性能服务(三)Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue--转载

    原文地址:http://maoyidao.iteye.com/blog/1663193 一个仅仅部署在4台服务器上的服务,每秒向Database写入数据超过100万行数据,每分钟产生超过1G的数据.而 ...

  6. 高性能队列Disruptor系列3--Disruptor的简单使用(译)

    简单用法 下面以一个简单的例子来看看Disruptor的用法:生产者发送一个long型的消息,消费者接收消息并打印出来. 首先,我们定义一个Event: public class LongEvent ...

  7. 强如 Disruptor 也发生内存溢出?

    前言 OutOfMemoryError 问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界.空指针等)来说这类问题是很难定位和解决的. 本文以最近碰到的一次线上内存溢出的定位.解决问题的方式展开 ...

  8. 构建高性能服务 Java高性能缓冲设计 vs Disruptor vs LinkedBlockingQueue

    一个仅仅部署在4台服务器上的服务,每秒向Database写入数据超过100万行数据,每分钟产生超过1G的数据.而每台服务器(8核12G)上CPU占用不到100%,load不超过5.这是怎么做到呢?下面 ...

  9. 采用CAS算法 实现高性能的Disruptor 完成多线程下并发、等待、先后等操作

    来源:https://blog.csdn.net/tianyaleixiaowu/article/details/79787377 拓展: https://www.jianshu.com/p/d24b ...

随机推荐

  1. 【Java】抽象类、接口

    什么是抽象类? 特点: - 抽象类几乎普通类一样,除了不能实例化 - 不能实例化不代表没有构造器,依然可以声明构造器,便于子类实例化调用 - 具有抽象方法的类,一定是抽象类 abstract 抽象的 ...

  2. JMock2入门

    说明:学习JMock官网的Getting Started的笔记 例子:为测试publish/subscribe发布/订阅信息系统的publisher(发布者),mock subscriber(订阅者) ...

  3. Multiple Books多账薄

    有些公司因管理需要配置多本账薄,比如管理帐和PRC,那么在Epicor 10中如何实现呢? 1创建 new Book: 2 created a map: Financial Management -& ...

  4. 数据类型、运算符、Scanner的使用

              一.常见的基本数据类型      数值型  byte(最小,2字节)      short(4字节) int (默认 8字节)    long(16字节)      浮点型   f ...

  5. PHP出现SSL certificate:unable to get local issuer certificate的解决办法

    当本地curl需要访问https时,如果没有配置证书,会出现SSL certificate: unable to get local issuer certificate错误信息. 解决办法: 1.下 ...

  6. Mysql使用终端操作数据库

      使用终端操作数据库       1.如何查看有什么数据库?     show databases;        2.如何选择数据库?    use databasesName;       3. ...

  7. Springboot:第一个Springboot程序(一)

    1.创建Springboot项目 选择创建Springboot项目: 填写项目基本信息: 选择Springboot版本以及web依赖(内嵌tomcat): 创建完成: 创建完成后 等待构建maven项 ...

  8. docker配置dns与容器的访问控制(6)

    Docker 没有为每个容器专门定制镜像,那么怎么自定义配置容器的主机名和DNS配置?秘诀就是它利用虚拟文件来挂载到容器的3个相关的配置文件. 进入容器内使用mount命令可以看到挂载信息,这种机制可 ...

  9. centos7与8的区别

    1.关于内核版本:RHEL8采用4.18.0-xRHEL7采用3.10-0-x 2 网络时间同步 RHEL8 只使用Chronyd,不支持NTP部署. RHEL7Chronyd与NTP两者都支持 3. ...

  10. [Inno Setup] 在 File Section 之前解压文件

    Prototype: procedure ExtractTemporaryFile(const FileName: String); Description: Extracts the specifi ...