我们之前聊过,一般来说提升系统性能有三板斧,缓存、异步、并行。
缓存主要是为了减少IO,利用RAM提升获取数据速度,一般情况下能解决绝大多数的问题,并且如果缓存框架设计良好,基本上不用开发人员关注内部技术,可以直接傻瓜式使用。
异步处理则稍显复杂,但是对于一个访问量高,且流程处理复杂的系统来说,是非常重要的手段。
新世纪初期,很多银行以及交易所,都使用的MQ进行通讯,就是应用的异步的理念。
1
异步
什么叫异步?
很简单,就是不直接实时返回结果,而是先将指令储存起来,内部处理完成之后,再反馈结果(或者提供查询接口)。
解除了对于实时返回结果的依赖后,系统内部就可以自己应用各种方式去增加处理效率。
举个通俗的例子,武松打虎前要吃饭喝酒,店小二接收了这个吃喝的“指令”,殷勤的将武松迎入店内。然后将这个“指令”告知厨房后就去迎接别的客人去了。
店小二就是接收命令的线程,而厨师则是工作线程。
拿我们来进行网上购物来说,但当用户按下“购买”按钮时,他实际上是触发了一个非常复杂的异步流程,一次真实的购买行为,而且还在现实中要将商品送到客户的家门口,这些远远超出了最初按下按钮那个行为的范围。
因此将软件拆散成异步流程就让我们可以将要解决的不同问题拆分开,让我们可以面对一个本来就是异步的世界。
我们来讲一下几种异步的常用模式及框架。
MQ消息队列
这是一种典型的使用外部独立中间件以进行异步的方式。
可以使用业内常见的ActiveMQ、RocketMQ等中间件。
本文就不再赘述。
事件驱动——Guava EventBus。
在研究具体例子之前我们先要澄清三个基本的概念,即服务之间相互交互的三种机制:命令(Command)、事件(Event)和查询(Query)。
事件的强大在于它们既是事实又是触发器。外部的数据可以被系统里面的任何服务重用。但从服务的角度看,事件对系统造成的耦合度要比命令和查询低,这一点非常重要。
服务之间相互交互的三种机制是:
命令:是一个动作,是一个要求其它服务完成某些操作的请求,它会改变系统的状态。命令会要求响应。
事件:既是事实又是触发器,用通知的方式向外部表明发生了某些事。
查询:是一个请求,查看是否发生了什么事。重要的是,查询操作没有副作用,它们不会改变系统的状态。
事件驱动服务的五个主要好处是:
-
解耦:打破阻塞式调用的长链,拆分同步工作流。代理节点解耦服务,这样就可以更容易地加入新服务,或改进现有的。
-
离线与异步工作流:当用户按下一个按钮后会发生许多事。有些是同步的,有些是异步的。对后者和前者的设计都垂手可得。
-
状态迁移:事件成了系统内的数据集。流提供了非常有效的方法来实现数据分发,这样就可以在一个受限的上下文内部重组和查询。
-
连接:不同的服务可以更容易地组合、连接和扩大数据集。连接操作都是在本地完成的,速度很快。
-
可追踪性:在有了一个集中的、不可变的清单来记下每一次变更之后,调试分布式系统的“谋杀之迷”问题就很容易了。
对于内存级事件框架,推荐应用Guava Eventbus。
Future模式
该模式的核心思想是异步调用. 有点类似于异步的ajax请求.当调用某个方法时, 可能该方法耗时较久, 而在主函数中也不急于立刻获取结果.因此可以让调用者立刻返回一个凭证, 该方法放到另外线程执行,后续主函数拿凭证再去获取方法的执行结果即可, 其结构图如下:
jdk中内置了Future模式的支持, 其接口如下:
这种模式在Netty等框架中有非常经典广泛的而实现。
Reactor模式反应器模式 & Proactor模式
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
-
Reactor模式——《Reactor模式,或者叫反应器模式 – daimojingdeyu》
并发系统常使用reactor模式,代替常用的多线程的处理方式,节省系统的资源,提高系统的吞吐量。
Reactor如何处理点菜这个问题呢:
老板发现,客人点菜比较慢,大部服务员都在等着客人点菜,其实干的活不是太多。老板能当老板当然有点不一样的地方,终于发现了一个新的方法,那就是:当客人点菜的时候,服务员就可以去招呼其他客人了,等客人点好了菜,直接招呼一声“服务员”,马上就有个服务员过去服务。嘿嘿,然后在老板有了这个新的方法之后,就进行了一次裁员,只留了一个服务员!这就是用单个线程来做多线程的事。
说白了就是1个服务员(线程),服务多个客户(任务)。
同步和异步是针对应用程序和内核的交互而言的; 同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪, 异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。
阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式; 阻塞方式下读取或者写入函数将一直等待, 非阻塞方式下,读取或者写入函数会立即返回一个状态值。
首先来看看Reactor模式,Reactor模式应用于同步I/O的场景。我们以读操作为例来看看Reactor中的具体步骤:读取操作:
-
应用程序注册读就需事件和相关联的事件处理器
-
事件分离器等待事件的发生
-
当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
-
事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
下面我们来看看Proactor模式中读取操作和写入操作的过程:读取操作:
-
应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
-
事件分离器等待读取操作完成事件
-
在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
-
事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步 需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
定时任务与批处理框架
不再赘述。
2
并行
我们所谓的并行,一般指将一个顺序执行的任务,拆解为多个并行执行的任务,然后多个独立执行的线程结果汇总成统一结果。
生产者消费者模式
生产者-消费者模式是一个经典的多线程设计模式. 它为多线程间的协作提供了良好的解决方案。
在生产者-消费者模式中,通常由两类线程,即若干个生产者线程和若干个消费者线程。
生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务。
生产者和消费者之间则通过共享内存缓冲区进行通信, 其结构图如下:
一般使用BlockingQueue作为数据缓冲队列, 他是通过锁和阻塞来实现数据之间的同步, 如果对缓冲队列有性能要求, 则可以使用基于CAS无锁设计的ConcurrentLinkedQueue.
Master-Worker模式
分而治之思路的一种,它可以将一个大任务拆解为若干个小任务并行执行, 提高系统吞吐量。
该模式核心思想是系统由两类进行协助工作: Master进程, Worker进程.Master负责接收与分配任务, Worker负责处理任务. 当各个Worker处理完成后, 将结果返回给Master进行归纳与总结。
主要思路,是通过两个数据结构存放结果。
ForkJoin线程池.
该线程池是jdk7之后引入的一个并行执行任务的框架, 其核心思想也是将任务分割为子任务, 有可能子任务还是很大, 还需要进一步拆解, 最终得到足够小的任务.将分割出来的子任务放入双端队列中, 然后几个启动线程从双端队列中获取任务执行.子任务执行的结果放到一个队列里, 另起线程从队列中获取数据, 合并结果.
也是分而治之思路的一种,它可以将一个大任务拆解为若干个小任务并行执行, 提高系统吞吐量。
3
线程池大小设置以及锁
在上面的并发、并行等处理节点中,有一个关键的问题,就是线程池数量的设置问题:
线程池参数设置
在如今的多核处理器时代,多线程技术发挥着巨大的作用,尤其对于大批量处理同类型IO密集型的任务,例如全库全表查找数据时,多线程是提升速度和性能的利器。
但平时的开发工作中,我们可能更加关注的是线程池的使用,线程数设置多大啊?队列大小(corePoolSize maxPoolSize queueCapacity) 设置多大啊,等问题。
1. 线程池中执行的任务性质。
计算密集型的任务比较占cpu,所以一般线程数设置的大小 等于或者略微大于 cpu的核数;但IO型任务主要时间消耗在 IO等待上,cpu压力并不大,所以线程数一般设置较大,例如 多线程访问数据库,数据库有128个表,可能就直接考虑使用128个线程。
-
CPU使用率。
当线程数设置较大时,会有如下几个问题:第一,线程的初始化,切换,销毁等操作会消耗不小的cpu资源,使得cpu利用率一直维持在较高水平。第二,线程数较大时,任务会短时间迅速执行,任务的集中执行也会给cpu造成较大的压力。第三, 任务的集中支持,会让cpu的使用率呈现锯齿状,即短时间内cpu飙高,然后迅速下降至闲置状态,cpu使用的不合理,应该减小线程数,让任务在队列等待,使得cpu的使用率应该持续稳定在一个合理,平均的数值范围。所以cpu在够用时,不宜过大,不是越大越好。可以通过上线后,观察机器的cpu使用率和cpu负载两个参数来判断线程数是否合理。可通过命令查看cpu使用率是否主要花在线程切换上。cpu负载是正在执行的线程和等待执行的线程之和,注意这个等待不是指线程的wait那种等待,而是指线程处于running状态但是还没有被cpu调度的等待,负载较高,意味着cpu竞争激烈,进而说明线程设置较大,在抢cpu资源。负载的值一般约等于 cpu核数 是比较合理的数值。
-
内存使用率。
线程数过多和 队列的大小都会影响此项数据,队列的大小应该通过前期计算线程池任务的条数,来合理的设置队列的大小,不宜过小,让其不会溢出,因为溢出会走拒绝策略,多少会影响性能,也会增加复杂度,因为你得好好考量你的拒绝策略的选择,拒绝策略包括 AbortPolicy(抛异常), CallerRunsPolicy(主线程执行) 和 DiscardPolicy(丢弃)。也不宜多大,过大用不上,还会消耗较大的内存。
-
下游系统抗并发的能力。
多线程给下游系统造成的并发等于你设置的线程数,例如如果是多线程访问数据库,你就等考虑数据库的连接池大小设置,数据库并发太多影响其qps,会把数据库打挂等问题。如果访问的是下游系统的接口,你就得考虑下游系统是否能抗的住这么多并发量,不能把下游系统打挂了。
锁
Synchronized关键词不再赘述。
另外,还可以使用ReentrantLock,性能上差不多,但是使用比较灵活。
3
JavaCore & HeapDump & Java性能
另外说明一点,既然使用了这些增强性能的手段,说明很可能出现一些性能问题。
简单介绍如下:
Java的CPU占用高问题
Java程序很耗CPU是比较好分析的,有这么几步:
1.通过top命令(top -H pid)查看CPU使用率高的线程;
2.将这个线程号转换为16进制;
3.使用jps查看服务器的Java进程号;
4.使用jstack [进程号] 打印当前的进程堆栈;(jstack -l pid)
5.从打印的信息中,找到第2步得到的线程号,看看这个线程在做什么。不一定一次就能抓准线程状态,可以第1步时多记几个线程。
JavaCore/HeapDump
可以直接kill命令后,生成javacore文件,然后使用jca工具分析。(java -Xmx200m -jar jca401.jar)
原文始发于微信公众号(架构突围):Java并发、异步多线程及Javadump性能分析工具全解
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/170133.html