第五章 并行模式与算法

20 篇文章 0 订阅

第五章 并行模式与算法

5.1 探讨单例模式

  • 单例模式的好处
    • 对于频繁使用的对象,可以省略new操作花费的时间,特别是重量级对象
    • 由于new操作的次数减少,因而对系统内存的使用频率会降低,减轻GC压力,缩短GC停顿时间
  • 单例实现:
    1. 静态变量直接实例化
      • 缺点:时间不可控制,如果存在其他静态变量,在这些变量被引用时,便会被创建。
      • 优点:对于并发不需要加锁
    2. 通过同步方法创建对象
      • 缺点:并发下性能会有一定影响
      • 优点:可以控制对象创建时间
    3. 内部类实现
      • 优点:既可以控制对象创建时间,也可以解决并发加锁问题。

5.2 不变模式

  • 一旦对象被创建,其内部的状态就是不可变的。通过回避问题而不是解决问题的态度来处理多线程并发访问控制。
  • 与只读对象的区别:不变对像比只读对象更具有强一致性,只读对象只是不可以被其他线程改变,但可以让其本身对属性进行修改。
  • 使用场景:
    • 当对象创建后,其内部状态和数据不再发生任何变化
    • 对象需要被共享,被多线程频繁访问
  • 不变模式的实现:
    • 去除setter方法以及所有修改自身属性的方法
    • 将所有属性设置为私有,并标记final,确保其不可修改
    • 确保没有子类可以重载修改它的行为
    • 有一个可以创建完整对象的构造函数

5.3 生产者-消费者模式

  • 通过共享内存缓冲区进行通信,核心组件也是它,通过它可以缓解生产者和消费者的性能差
  • BlockingQueue充当了共享内存缓冲区,用于维护任务和数据队列。

5.4 高性能的生产者-消费者:无锁的实现

  • BlockingQueue并不是一个高性能的实现,它完全使用锁和阻塞等待来实现线程间的同步。
  • 使用CAS实现生产者-消费者模式框架Disruptor。性能比BlockingQueue至少高一个数量级以上。

5.4.1 无锁的缓存框架:Disruptor

  • 基于环形队列实现,因此队列的总大小必须事先指定,无法动态扩展。Disruptor要求将数组大小设置为2的整数次方,通过sequence&(sequence-1)便可以快速定位实际的元素位置,比取余操作快很多。
  • 大小固定的环形队列的另一个好处是可以做到完全的内存复用。

5.4.2 用Disruptor实现生产者-消费者案例

EX:参考196

5.4.3 提高消费者的响应时间:选择合适的策略

  • WaitStrategy几种实现:
    • BlockingWaitStrategy:默认策略,和使用BlockingQueue非常类似,都使用了锁和条件进行数据的监控和线程的唤醒。最节省cpu但高并发下性能最差的一种。
    • SleepingWaitStrategy:也是对cpu使用率非常保守,循环中不断等待数据,进行自旋等待,如果不成功使用Thread.yield让出cpu,并最终使用LockSupport.parkNanos(1)进行线程休眠,以确保不占用太多的cpu数据。因此可能会产生比较高的平均延时。适合延时不敏感的场合,好处是对生产者线程的影响最小。典型场景异步日志。
    • YieldingWaitStrategy:适用于低延时场景,消费者会不断循环监控缓冲区变化,在死循环内部,使用Thread.yield让出cpu给别的线程执行时间。该策略最好有多于消费者线程数量的逻辑cpu数量,否则整个应用程序可能都会受到影响。
    • BusySpinWaitStrategy:最疯狂的等待策略,一个死循环,让消费者监控缓冲区的变化,因此,会吃掉所有的cpu资源。适用于延时非常苛刻的场景。因此物理cpu数量必须大于消费者线程数,如果在一个物理核上使用了超线程技术模拟了两个逻辑核,则另外一个逻辑核会受到这种超密集计算的影响而不能正常工作。

5.4.4 CPU Cache 的优化:解决伪共享问题

  • 为了提高cpu的速度,cpu有一个高速缓存cache,在高速缓存中,读写数据最小单位为缓存行(cache line),它是从主存(memory)复制到缓存(cache)的最小单位,一般32到128字节。
  • 问题:
    • 假设X和Y两个变量在同一个缓存行中,CPU1上的线程对X进行了修改,会导致缓存行实效,从而导致Y也失效,此时如果CPU2读取Y则会导致Cache无法命中。这种情况如果频繁发生,性能将会大大降低。
    • 解决方法:一种可行的方法是在变量X的前后空间都先占据一定的位置(把它叫做padding),使得缓存行中只有变量X。
    • JDK不同版本会对这种情况进行优化。
    • Disruptor也考虑了这个问题,因此其环形缓冲区RingBuffer,内部实现上,实际产生的数组大小是缓冲区大小,再加上两倍的BUFFER_PAD,使得整个数组被载入Cache时不受其他变量影响。

5.5 Future 模式

核心思想就是异步调用

5.5.1 Future模式的主要角色

  • Main:客户端启动程序
  • Client:返回Data对象,立即返回FutureData,并开启ClientThread线程装配RealData
  • Data:返回数据的接口
  • FutureData:Future数据,构造很快,但是是一个虚拟的数据,需要装配RealData
  • RealData:真实数据,其构造是比较慢的

5.5.2 Future模式的简单实现

  • 核心接口Data,两个实现FutureData和RealData,第一个是用来提取RealData的契约,因此会立即返回得到,第二个是我们真实想要的数据。当调用FutureData的getResult方法时,如果实际数据没准备好,则会阻塞,等待RealData准备好。

5.5.3 JDK中的Future模式

  • 定义了Future接口,其get方法会返回真实数据,如果真实数据没准备好会阻塞。RunnableFuture继承了Future和Runnable接口,run方法构造真实数据,FutureTask负责具体的实现,它内部有个Sync内部类,会委托一些实质性工作给它,Sync最终会调用Callable接口,完成实际的数据组装工作。Callable接口只有一个call方法,返回需要构造的实际数据。

5.6 并行流水线

  • 对于无法进行并行运算的业务,将各个业务拆分为各个小任务,各个任务之间协调好依赖性,同流水线一般,以此来处理大量数据的情况。EX:参考213

5.7 并行搜索

EX:参考216

5.8 并行排序

5.8.1 分离数据相关性:奇偶交换排序

  • 分为奇索引排序和偶索引排序,来解决数据相关问题,实现并行排序.
  • EX:参考220

5.8.2 改进的插入排序:希尔排序

  • 将整个数组根据索引间隔h分割为若干个子数组,每次排序总是交换间隔为h的两个元素,在完成每一组排序后,递减h的值,直到h为1。优点是即使较小的元素在数组末尾,由于每次移动都以h为间隔,因此也会跟快被移动到前面。
  • EX:参考223

5.9 并行算法:矩阵乘法

  • 矩阵计算工具:jMatrices
  • EX:参考227

5.10 准备好了再通知我:网络NIO

5.10.1 基于Socket的服务端的多线程模式

EX:参考231

5.10.2 使用NIO进行网络编程

EX:参考236

5.10.3 使用NIO来实现客户端

EX:参考243

5.11 读完了再通知我:AIO

异步IO,Asynchronized

5.11.1 AIO EchoServer实现

EX:参考245

5.11.2 AIO Echo 客户端实现

EX:参考248

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值