Loading

Java并发学习之执行任务

一.执行任务

其实大多数的并发应用程序都是围绕着“ 执行任务“来构造的 。

1.什么是任务?
任务通常是一些抽象且离散的工作单元,我们通过把应用程序的工作分解到多个任务中简化程序的组织结构,提供一种自然的事务边界来优化错误恢复过程,以及提供一种自然的并行工作结构来提升并发性。

2.在线程中执行任务
当围绕执行任务来设计应用程序时:
1)第一步就是要找出清晰的任务边界。

在理想情况下,各个任务之间是相互独立的,一个任务不依赖于其他任务的状态,结果或者边界效应。 独立性有助于实现并发,因为如果存在足够多的处理资源,那么这些独立的任务都可以并行执行。 为了在调度与负载均衡等过程中实现更高的灵活性,每项任务还应该表示应用程序的一小部分处理能力。

在实际情况下,服务器应用程序应该尽可能的支持很多用户,并且响应速度尽可能的快,而且当负荷过载时,应用程序的性能应该是逐渐降低而不是直接失败,如果我们想要实现上述的功能,应该选择清晰的任务边界以及明确的任务执行策略。

大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。
将独立的请求作为任务边界,既可以实现任务的独立性,又可以实现合理的任务规模,例如在向邮件服务器提交一个消息后得到的结果并不会受其他正在处理的消息影响,而且在处理单个消息时通常只需要服务器总处理能力的很小一部分。

3.串行地执行任务
我们可以有多种策略来调度任务,最简单的调度策略就是在单个线程中串行地执行各项任务,例如:

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

上面代码很简单,在理论上它是正确的,但在实际的生产环境中的执行性能却很差,因为它每次只能处理一个请求。主线程在接受连接与处理相关请求等操作之间不断地交替运行,当服务器正在处理请求时,新到来的连接必须等待直到请求处理完成。 如果handleRequest方法很快就返回,那么这样是可以的,但是如果handleRequest是个非常耗时的操作,那么这样是绝对不可行的。

所以串行处理通常都无法提高吞吐率或快速响应性,故我们应该尽量避免串行策略。

4.显式地为任务创建线程

对于上面的串行策略的改进,我们也许可以通过为每个请求创建一个新的线程来提供服务,这样就实现了更高的响应性。
例如下面:

class ThreadPerTaskWebServer{
  public static void main(String [] args) throws IOException{
      ServerSocket socket= new ServerSocket(80);
      while(true){
         final Socket connection = socket.accept();
         Runnable task=new Runnable(){
            public void run(){
               handleRequest(connection);
            }
         };
         new Thread(task).start();//为每个请求创建一个线程并开始
      }
  }
}

对于上面的程序,主线程仍然不断的交替执行 接受外部连接 与 分发处理请求 ,但对于每个连接主线程都会循环的创建一个新的线程来处理请求,而不是在主循环中处理。

由此我们可以得到三个结论:
1)任务处理过程中从主线程中分离出来,使得主循环能够更快地重新等待下一个到来的连接,这使得程序在完成前面的请求之前会很快地接受新的请求,从而提高响应速度。

2)任务并发进行,从而同时服务多个请求。这样就提高了程序的吞吐量。

3)任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。

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

那么新问题来了:
5.上面的方法是完美的吗? 我们能无限制的创建线程来解决多个请求吗?

我们需要知道线程的下面知识:
1)线程生命周期的开销非常高。 线程创建与销毁是有代价的,根据平台的不同,实际的开销也不同,所以线程的创建过程需要时间,延迟处理的请求,并且需要JVM和操作系统提供一些辅助操作。
所以如果请求的到达速率非常高且请求的处理过程是轻量级的,那么为每个请求创建一个线程显得有些得不偿失。

2)资源消耗。 活跃的线程会消耗系统资源,这是我们都知道的,尤其是内存。所以如果可运行的线程数量多于可用处理器的数量,那么这些线程将闲置。大量的闲置线程会占用内存,这会给垃圾回收器带来压力,而且大量线程在竞争CPU资源时还将产生其他的性能开销。
如果我们已经拥有足够多的线程使所有CPU保持忙碌状态,那么再创建更多的线程反而会降低性能。

3)稳定性。 在可创建线程的数量上存在一些限制。这个限制值将随着平台的不同而不同,并且受多个因素制约,包括:JVM的启动参数,Thread构造函数中请求的栈大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么很可能抛出OutOfMemoryError异常,如果我们要避免这种情况,我们可以通过构造程序来避免超出这些限制。

所以我们有下面结论:
在一定范围内,增加线程可以提高系统的吞吐率,但如果超出了这个范围,再创建更多的线程只会降低程序的执行速度,要避免这种风险,我们就应该对应用程序可以创建的线程数量进行限制,并且全面地测试应用程序,从而确保在线程数量达到限制时,程序也不会耗尽资源。

posted @ 2020-11-16 15:59  文牧之  阅读(39)  评论(0)    收藏  举报  来源