Java并发应用编程之任务执行分析

概述

DougLea等人写的《Java并发编程实战》中是这样描述任务的:“在多数的并发应用程序中,都是围绕着“任务执行”来构造的,而任务通常是一些抽象且离散的工作单元,通过把应用程序的工作分解到多个任务中,可以简化应用程序的组织结构,提供一种自然的事务边界来优化错误恢复过程,以及提供一种自然的并行工作结构来提升并发性。”这段话理解起来就是:我们将一个很复杂的工作A,分解成很多的小任务,然后让这些小任务同时开始干自己的事情。当这些小任务都干完了后再合并成我们要完成的最终的那个复杂工作A。而如何合理的将这个复杂的任务A合理的拆解成一个个的小任务,以及如何安排执行这些任务,最终高效的得到正确的结果成为我们要考虑的重要问题

1.任务执行边界划分

当我们要完成一个复杂的任务时,或者是一个耗时很长的任务时,我们往往会将其划分为多个任务并发的去执行。在最理想的情况下,我们划分的这些任务之间是相互独立的:即划分的任务之间不相互依赖,一个任务的执行不依赖其他任务的状态,计算结果。任务之间的独立性有助于实现并发。如果有足够的处理器资源,那么独立的任务可以并行执行。

在正常的负载下,我们的应用程序应该要表现出良好的吞吐量和快速响应性。应用的提供商都希望程序能够尽可能的支持更多的用户,降低每个用户的服务成本,而用户则希望获得尽快的程序响应。而且,当负荷过载时,应用程序的性能应该是逐渐缓和的降低,而不是直接以下就异常闪退。要实现这个目标就需要我们能划分出清晰的任务边界以及明确的任务执行策略。如下图所示:

在这里插入图片描述
理想情况下,划分的三个子任务是独立的,互相不依赖的,这样我们就可以让三个子任务同时进行运算,这样复杂任务A的运算就会更快。用户也会很快的得到响应。

2.服务器应用程序的想法和实现

在多数的服务器应用程序中都提供了一种自然的任务边界划分方式,每个用户的一次请求为一个边界,即一次请求对应一个独立的任务。Web服务器,邮件服务器,文件服务器,EJB容器以及数据库服务器等,这些服务器都能通过网络接受远程客户的连接请求。每一次请求我们都把其作为一个任务处理,很明显,这些任务之间是独立的,比如,向邮件服务器提交一个消息后得到的结果,并不会受其他正在处理的消息的影响。

2.1 串行执行任务

在应用程序的开发过程中,我们可以通过多种策略来调度任务,最简单的方法就是在单个线程中串行执行各项任务,也就是说任务必须一个个执行,同一时间内,在该线程中只有一个任务在执行,伪代码如下:

public class WebServer {
    
    
    public static void main(String[] args) throws IOException {
    
    
        ServerSocket serverSocket = new ServerSocket(80);
        while (true){
    
    
            Socket connection = serverSocket.accept();
            handleRequest(connection);// 处理请求
        }
    }
}

上面的代码是无法商用的,因为它每次都只能处理一个请求,其他的请求到来时,假设服务器正在处理请求,那么就得等待直到服务器处理完上一个请求,假设每一个请求的处理都超级快,那么这种办法是可行的,但是现实世界中,服务器的情况是千变万化的。所以这种方式是非常不推荐的

而且更不可接受的是,假设用户的Web请求中包含了一组不同的运算与IO操作。服务器必须要等待IO和运算完成。这些操作可能会由于网络拥塞或者联通性的问题而被阻塞,在单线程的程序中,阻塞不仅会推迟当前请求完成的时间,而且还将彻底阻止等待中的请求被处理,如果请求阻塞的时间过长,用户会认为服务器是不可用的。

2.2 为每个请求创建线程来执行任务

上一节我们说到单线程去执行用户请求是很不科学的,所以我们假设通过为每个请求创建一个线程去执行它,会有问题吗?带着这个问题,我们一起来分析下:伪代码如下所示:

    public static void main(String[] args) throws IOException {
    
    
        ServerSocket serverSocket = new ServerSocket(80);
        while (true){
    
    
            Socket connection = serverSocket.accept();
            Runnable task = new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    handleRequest(connection);// 处理请求
                }
            };
            
            new Thread(task).start();
        }
    }

如上面的代码所示,每一个请求我们都创建了一个线程去处理,任务的处理过程从主线程中分离出来,使得主循环能够更快的等待下一个到来的连接。这使得程序在完成前面的请求之前可以提前接受新的请求,提高了程序的响应性。而且多个任务可以并行处理,也就是多个请求可以同时被处理,如果有多个处理器,或者是任务由于某种原因被阻塞,例如等待I/O完成,获取锁或者资源可用性等,程序的吞吐量将会得到提高,需要注意的是多个线程并行处理的时候,需要注意线程的安全性。

假设在正常的负载下,为每个请求任务分配一个线程的方法能提升程序串行执行的性能,只要是请求到达速率不超出服务器的请求处理能力,那么这种方法可以同时带来更快的响应性和更高的吞吐率。

那么在生产环境中,为每个任务分配一个线程这种方法就一定好吗?当然不是,尤其是当我们要创建大量的线程时,主要的缺陷有以下几点:

(1)线程生命周期的开销非常高
线程的创建和销毁并不是没有代价的,根据平台的不同,实际上的开销也会有所不同,线程的创建和销毁都需要时间
(2)线程的创建需要消耗资源
活跃的线程会消耗资源,特别是内存。如果可运行的线程数量多余可用处理器的数量没那么有些线程将无事可干,而白白浪费系统资 源。给垃圾回收器带来压力。相反,如果已经拥有了足够多的线程,使所有的CPU都处于忙碌状态,那么再创建线程反而会降低性能。
(3)稳定性
在可创建的线程数量上存在一个限制。这个限制值将随平台的不同而不同,并且受多个因素制约,包括JVM的启动参数,Thread构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么很可能会抛出OutOfMemoryError异常

所以综上所述,在一定的范围内,增加线程可以提高系统的吞吐率,但是如果超出了这个范围,再创建更多的线程指挥降低程序的执行速率,并且过多的创建一个线程,那么整个应用程序都将会崩溃,想要避免这种危险就应该对应用程序可以创建的线程数量进行限制。从而确保在线程达到限制时,程序也不会耗尽资源

猜你喜欢

转载自blog.csdn.net/zxj2589/article/details/131450123
今日推荐