提升并发程序性能(tps/qps)的几个技巧总结

引言

之前参加中间件比赛,以及一些日常开发的经验,在这里总结了一些提升程序性能(qps/tps)的技巧,持续更新。一些只适用与比赛而不适合实际工程的技巧我会用斜体 (only race) 标注

1、减小锁的粒度

案例:

  • ConcurrentHashMap采用分段锁提升了并发时map的性能
  • SkipList(跳表):跳表相比红黑树的优势就是,红黑树的一次插入删除操作经常会导致全局的调整,导致整棵树被锁,而跳表的插入和删除只涉及局部操作,所以可以减小锁的粒度,得到更好的性能。所以JDK中选择跳表实现有序的并发map,对应的类是ConcurrentSkipListMap和ConcurrentSkipListSet。Redis中的有序列表(zset)结构也是用跳表实现的

2、减小调度的粒度

传统的基于线程的调度模型调度粒度过大,往往导致频繁的上下文切换影响程序性能

案例:

  • 协程(Coroutine):在单线程中利用子程序的中断与返回模拟出类似多线程的效果,有点是省去了很多同步操作以及线程上下文切换的开销。kotlin,lua,python和Go语言都原生支持协程,Java也可以通过一些开源库模拟出来,比如Quasar
  • Actor模型:Scala的并发包中提供了一种比线程粒度更细的Actor模型,使用ForkJoinPool来调度线程资源,使用消息传递来代替共享变量的同步操作,降低了编程难度的同时提升了性能

3、避免伪共享

CPU的缓存都是以缓存行(通常为64B)为单位的,即使互相之间不需要同步的变量,只要位于同一个缓存行中,一个变量的改变也会导致整个CPU缓存的失效。

案例:

  • Disruptor的RingBuffer的对应策略:给RingBuffer中每个对象的前后增加padding(即8个long类型),这样就能保证这个对象是独立的位于CPU缓存行中了,同时为了避免JVM在动态编译的过程中优化掉这些padding变量,Disruptor会在序号管理器中返回对padding变量进行加减操作
  • Java新出现的注解@sun.misc.Contended(需要配合虚拟机参数-XX:-RestrictContended使用)可以更加优雅地解决伪共享问题

4、尽可能地重用对象,减少GC

案例:

  • netty中可以使用Recycler来实现对象池
  • Disruptor中的RingBuffer就是由可重用的对象组成,GC友好
  • 利用apache common pool重用对象

5、数据多版本(使用快照)

案例:

  • mysql使用MVCC控制并发,每个事务在undo日志有一个自己的数据快照,这样select语句就全部变成不需要加锁的快照读
  • JDK中也有类似思想的实现,即CopyOnWriteArrayList,在写入时会进行一次自我复制,这样就能够实现无等待的读操作,适用于读多写少的场景

6、将竞争分散

案例:

  • CLH锁算法:基于local-spin的思想,将标志位的竞争分散到了队列中每个节点的前继上,这样每个CPU都可以基于自己的缓存自旋
  • Java8新引入的LongAdder:将一个long拆分成多个部分,每个线程还需要在属于自己的一部分上进行加法即可,减少了竞争

7、尽量不要使用库 (only race)

这一条只适用于竞赛的情况,在竞赛中的往往线上资源的CPU不是很充足,而库中往往包含大量的判断校验等逻辑,比较浪费CPU,所以我们应该尽量将库中的核心代码手工抽取出来进行调用。

注:这里的库包括JDK

8、Less is More

很多东西并不是越多越好,特别是那些会占用资源的东西。

案例(比赛中实测的一些结果):

  • 单线程读写同一个硬盘往往比多线程快
  • 使用单条tcp连接往往比多条tcp连接快
  • 单个消费线程往往比多个消费线程快

9、不变模式

当一个对象不会再发生状态改变时,线程对这个对象的访问自然就不需要任何同步。

所以有一个技巧,当有多个线程尝试着访问同一个对象时,可以思考能否将这个对象拆成多份,并且其中有一些是只读的,这样只读的部分就可以应用不变模式,线程访问不需要任何同步操作。

案例:

  • LevelDB中当MemTable达到一定大小后不会直接将其落盘,而是而是先将其转换成不变的ImmutableMemTable,之后另一个线程将ImmutableMemTable落盘,写线程则在新建立的MemTable中写数据。这种将内存数据拆成MemTable和ImmutableMemTable的做法巧妙的避开了生成者与消费者之间的同步操作,方便了实现也提高了性能

10、线程亲和性

线程亲和性技术能够让开发人员将某个重要的线程直接绑定到某个CPU上面,保证这个重要的线程始终拥有CPU资源,不会因为时间片耗尽而被换出去。Java中有封装好的线程亲和性实现:https://github.com/OpenHFT/Java-Thread-Affinity

你可能感兴趣的:(聊聊技术)