JAVA高并发——生产者-消费者模式

本文介绍了生产者-消费者模型的基本概念,强调了无锁和高性能的重要性,并提到使用CAS和Disruptor框架来优化性能。特别关注了无锁缓存框架Disruptor,其环形队列设计减少了线程同步的复杂性并实现了内存复用。
摘要由CSDN通过智能技术生成

1、生产者-消费者模型

生产者-消费者模式是一个经典的多线程设计模式,它为多线程间的协作提供了良好的解决方案。在生产者-消费者模式中,通常有两类线程,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务。生产者和消费者之间则通过共享内存缓冲区进行通信。

下图展示了生产者-消费者模式的基本结构:
在这里插入图片描述
生产者-消费者模式的主要角色如下表所示:
在这里插入图片描述
下图显示了生产者-消费者模式的一种实现类图:
在这里插入图片描述
其中,BlockingQueue充当了共享内存缓冲区,用于维护任务或数据队列(PCData对象)。

PCData对象表示一个生产任务,或者相关任务的数据。生产者对象和消费者对象均引用同一个BlockingQueue实例。生产者负责创建PCData对象,并将它加入BlockingQueue中,消费者则从BlockingQueue中获取PCData对象。

基于上图,下面我们实现一个基于生产者-消费者模式的求整数平方的并行程序。生产者线程的实现如下,它构建PCData对象,并放入BlockingQueue中:
在这里插入图片描述
对应的消费者的实现如下,它从BlockingQueue中取出PCData对象,并进行相应的计算:
在这里插入图片描述
PCData对象作为生产者和消费者之间的共享数据模型,定义如下:
在这里插入图片描述
PCData对象作为生产者和消费者之间的共享数据模型,定义如下:
在这里插入图片描述
在主函数中,创建三个生产者和三个消费者,并让它们协作运行。在主函数的实现中,LinkedBlockingQueue作为BlockingQueue的实现类。
在这里插入图片描述
注意:生产者-消费者模式很好地对生产者线程和消费者线程进行了解耦,优化了系统整体结构。同时,由于缓冲区的存在,允许生产者线程和消费者线程存在执行上的性能差异,从一定程度上缓解了性能瓶颈对系统性能的影响。

2、高性能的生产者-消费者模式:无锁的实现

用BlockingQueue实现生产者和消费者是一个不错的选择。它可以很自然地实现作为生产者和消费者的内存缓冲区。但是BlockingQueue并不是一个高性能的实现,它完全使用锁和阻塞等待来实现线程间的同步。在高并发场合,它的性能并不是特别优越。就像之前我已经提过的:ConcurrentLinkedQueue是一个高性能的队列,但是BlockingQueue只是为了方便数据共享。

ConcurrentLinkedQueue的秘诀就在于大量使用了无锁的CAS操作。同理,如果我们使用CAS来实现生产者-消费者模式,也可以获得可观的性能提升。不过正如大家所见,使用CAS编程是非常困难的,但有一个好消息是,目前有一个现成的Disruptor框架,已经帮助我们实现了这个功能。

ConcurrentLinkedQueue的秘诀就在于大量使用了无锁的CAS操作。同理,如果我们使用CAS来实现生产者-消费者模式,也可以获得可观的性能提升。不过正如大家所见,使用CAS编程是非常困难的,但有一个好消息是,目前有一个现成的Disruptor框架,已经帮助我们实现了这个功能。

3、无锁的缓存框架:Disruptor

Disruptor框架是由LMAX公司开发的一款高效的无锁内存队列。它使用无锁的方式实现了一个环形队列(RingBuffer),非常适合实现生产者-消费者模式,比如事件和消息的发布。Disruptor框架别出心裁地使用了环形队列来代替普通线形队列,这个环形队列内部为一个普通的数组。对于一般的队列,势必要提供队列头部head和尾部tail两个指针,用于出队和入队,这样无疑增加了线程协作的复杂度。但如果队列是环形的,则只需要对外提供一个当前位置指针,利用这个指针既可以进行入队操作,又可以进行出队操作。由于环形队列的缘故,队列的总大小必须事先指定,不能动态扩展。为了能够快速从一个序列(sequence)对应到数组的实际位置(每次有元素入队,序列号就加1),Disruptor框架要求我们必须将数组的大小设置为2的整数次幂。这样通过sequence&(queueSize-1)就能立即定位到实际的元素位置index,这比取余(%)操作快得多。

上面的sequence&(queueSize-1)不太好理解,我在这里再简单说明一下。如果queueSize是2的整数次幂,则这个数字的二进制表示必然是10、100、1000、10000等形式。因此,queueSize-1的二进制则是一个全1的数字。这样它可以将sequence限定在queueSize-1范围内,并且不会有任何一位是浪费的。

下图显示了RingBuffer的结构。生产者向缓冲区中写入数据,而消费者从中读取数据。生产者写入数据时,使用CAS操作,消费者读取数据时,为了防止多个消费者处理同一个数据,也使用CAS操作保护数据。
在这里插入图片描述
这种固定大小的环形队列的另外一个好处就是可以做到完全的内存复用。在系统的运行过程中,不会有新的空间需要分配或者老的空间需要回收,可以大大减少系统分配空间及回收空间的开销。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值