1.1 异步编程模式
大家学Java这么久,应该都很清楚入口函数main,异步编程模式意味着在执行main函数的主线程下同时并行且非阻塞地运行一个或多个任务。这类似于一个小组负责一个项目,会在主任务进展过程中同时要求组里的一些人执行一些独立的相关任务,而主任务只需要相关任务的结果(在这里,大家可以联想一下守护线程和用户线程,守护线程会在主任务结束的时候随之结束,而用户线程则可能依然执行,就好像对方需要我们去做一件事,然而我们很认真地做完之后回来发现,对方已经结束了,不需要我们的结果了)。由上述介绍可知,这种异步编程模式要求我们根据小组里的人员配备进行任务的分配,在编程中也就是要平衡使用系统中的多核CPU,根据CPU的核数来并发地分配要执行的任务,从而做到最大限度地利用处理器来提高程序性能。
这种异步编程模式带给我们的最主要的好处就是,可使程序的性能和响应速度得到大幅提升。这也是我们一直所追求的目标,无论作为开发者还是消费者,我们都不希望一直处于等待、等待、再等待的状态。所以通过异步编程模式,消费者可以在明知道某个任务会消耗很长时间的情况下,通过无障碍地使用其他功能,仍能获得良好的体验。
1.1.1 并发
可以这么说,并发很好地利用了CPU时间片的特性,也就是操作系统在当前时间片内选择并运行一个任务,接着在下一个时间片内选择并运行另一个任务,并把前一个任务设置成等待状态。其实这里想表达的是,并发并不意味着并行。
具体介绍几种情况,分别如下。
● 有时候多线程执行会提高应用程序的性能,而有时候反而会降低应用程序的性能。这在JDK中Stream API的使用上体现得很明显。如果任务量很小,而我们又使用了并行流,反而降低了性能。
● 我们在多线程编程中可能会同时开启或者关闭多个线程,这会产生大量的性能开销,也降低了程序性能。
● 当我们的线程同时都处于等待I/O的过程中时,并发可能会阻塞CPU资源,其造成的后果不仅是用户等待结果,而且会浪费CPU的计算资源。
● 如果几个线程共享了一个数据,情况就变得有些复杂了,我们需要考虑数据在各个线程中状态的一致性。为了达到这个目的,我们很可能会使用Synchronized或者lock。
现在,你应该对并发有一定的认知了吧。并发确实是一个好东西,但并不一定会实现并行。并行指的是在多个CPU核上同一时间运行多个任务,或者一个任务分为多块执行(如ForkJoin)。在单核CPU上就不要考虑并行了。
补充一点,实际上,多线程就意味着并发,但是并行只发生在将这些线程于同一时间调度分配到不同CPU核上执行的时候。也就是说,并行是并发的一种特定形式。
1.1.2 并行开发初探
现实中,再大的项目落实到细节上其实都是由多人协作完成的,而每个人在做自己的工作的时候只能一步步地做。可以说项目会被分解成一个个模块,由几个部门一同开发,而在每个部门中其又会被分成一个个子任务,由每个人来负责,最后协作汇总。
我们都知道,CPU也是一个接着一个指令地执行任务的。根据上面描述的场景,我们可以做类比,这里所谓的并行开发就是将一个大任务拆成许多部分来同时执行的。这样就更好地利用了多处理器多核环境,而拆分的每个任务都是独立执行的,并且每个任务彼此之间的执行顺序也没什么关系。通过并行执行,大型问题的解决速度直接快了很多,同时也可以更加高效地利用内存。
基于线程模型,我们可以通过良好的控制来实现用户访问功能的流程,但是为了后台服务器能够更好地进行数据的处理,可以根据需要将并发处理设定为一个开关操作。此操作可自动根据任务的数量来分配线程,同时我们需要一套处理数据的模型。从架构层面,我们拿消息中间件来说,消息中间件连接的各个工程作为一个整体就可以看成数据的一种流向处理,客户端通过订阅生产所设定的Topic来做数据的接收处理。而往小的API方向考虑,可以试着理解JDK中流的概念或者Linux命令中管道的概念(只不过管道和流都是基于推的模式),从中抽象出一个更好用的模型,然后将并发处理的开关操作作为其中的一环加入即可。这时,响应式编程(Reactive Programming)就呼之欲出了。