本文主要讲解Java并发相关的内容,包括锁、信号量、堵塞队列、线程池等主要内容。
在讲述怎么利用多线程的情况下,我们先看一下采用多线程并发的优缺点。
优点提高资源利用率
如读取一个目录下的所有文件,如果采用单线程模型,则从磁盘读取文件的时候,大部分CPU用于等待磁盘去读取数据。如果是采用多线程并发执行,则CPU可以在等待IO的时候去做其他的事情,以提高CPU的使用率,减少资源的浪费。
程序响应速度好
单线程模型下,假设一个http请求需要占用大量的时间来处理,则其他的请求无法发送请求给服务端。而多线程模式下,监听线程把请求传递给工作者线程,然后立刻返回去监听,可以去接收新的请求,而工作者线程则能够处理这个请求并发送一个回复给客户端。明显响应速度比单线程模型要好得多。
程序设计复杂度
多线程情况下,需要考虑线程间的通信、共享资源的访问,相对而言要比单线程程序负责一些。
上下文切换开销大
当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行。这种切换称为“上下文切换”。CPU会在一个上下文中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。尤其是当线程数量较多时,这种开销很明显。
资源消耗
线程在运行的时候需要从计算机里面得到一些资源。除了CPU,线程还需要一些内存来维持它本地的堆栈。它也需要占用操作系统中一些资源来管理线程
并发系统可以采用多种并发编程模型来实现。并发模型指定了系统中的线程如何通过协作来完成分配给它们的作业。不同的并发模型采用不同的方式拆分作业,同时线程间的协作和交互方式也不相同。
并行工作者在并行工作者模型中,委派者(Delegator)将传入的作业分配给不同的工作者。每个工作者完成整个任务。工作者们并行运作在不同的线程上,甚至可能在不同的CPU上。
假设电商系统中的秒杀活动采用了并行工作者模型,订单->财务->仓储->物流,工作者A拿到订单请求,然后负责支付流程,查询仓储情况,直到发货。
在Java应用系统中,并行工作者模型是最常见的并发模型,java.util.concurrent包中的许多并发实用工具都是设计用于这个模型的。
优点
易于理解,可以添加更多的工作者来提高系统的并行度
缺点
共享状态可能会很复杂
在上面的电商系统中,由于共享的工作者经常需要访问一些共享数据,无论是内存中的或者共享的数据库中的。
在等待访问共享数据结构时,线程之间的互相等待将会丢失部分并行性。许多并发数据结构是阻塞的,意味着在任何一个时间只有一个或者很少的线程能够访问。这样会导致在这些共享数据结构上出现竞争状态。在执行需要访问共享数据结构部分的代码时,高竞争基本上会导致执行时出现一定程度的串行化。
无状态的工作者
每次都重读需要的数据,将会导致速度变慢,特别是状态保存在外部数据库中的时候。
任务顺序是不确定的
作业执行顺序是不确定的,无法保证哪个作业最先或者最后被执行。如A先下单,B后下单,不根据时间进行业务逻辑的判断,不能有可能B先于A收到货。
流水线模式中,每个工作者只负责作业中的部分工作。当完成了自己的这部分工作时工作者会将作业转发给下一个工作者。每个工作者在自己的线程中运行,并且不会和其他工作者共享状态。也称反应器系统,或事件驱动系统。
以秒杀为例,工作者A执行订单的处理,工作者B执行支付,工作者C检查仓储,工作者D负责物流,分工明确,各司其职。
在实际应用中,作业有可能不会沿着单一流水线进行。由于大多数系统可以执行多个作业,作业从一个工作者流向另一个工作者取决于作业需要做的工作。在实际中可能会有多个不同的虚拟流水线同时运行。
作业甚至也有可能被转发到超过一个工作者上并发处理。比如说,作业有可能被同时转发到作业执行器和作业日志器。下图说明了三条流水线是如何通过将作业转发给同一个工作者(中间流水线的最后一个工作者)来完成作业:
优点
无需共享的状态
工作者之间无需共享状态,意味着实现的时候无需考虑所有因并发访问共享对象而产生的并发性问题