Java并发编程实战(学习笔记五 第六章 任务执行)

本文介绍了Java并发编程中任务执行的策略,包括串行执行任务、显式创建线程以及无限制创建线程的优缺点。重点讲解了Executor框架,如基于Executor的Web服务器、执行策略、线程池以及Executor的生命周期。通过Executor,可以实现任务的解耦和灵活的执行策略,例如使用固定长度线程池、可缓存线程池和定时线程池。文章还探讨了延迟任务与周期任务的处理,以及如何通过Callable和Future实现任务结果的获取。最后,通过页面渲染器的例子展示了如何利用并行性提高性能,使用CompletionService和限时get方法优化任务执行。
摘要由CSDN通过智能技术生成

6.1 在线程中执行任务

清晰的任务边界

在理想情况下,各个任务之间是相互独立的:任务并不依赖于其他任务的状态,结果或边界效应。
独立性有助于实现并发。

大多数服务器应用程序都提供了一种自然的任务边界选择方式,以独立的客户请求为边界。

6.1.1 串行地执行任务(Executing Tasks Sequentially【继续地】)

在应用程序中可以通过多种策略来调度任务,而其中一些策略能够更好地利用潜在并发性。

最简单的策略就是在单个线程中串行地执行各项任务。

SingleThreadWebServer将串行地处理它的任务(通过80端口接收到HTTP请求)。

//         6-1  串行的Web服务器(并不好)
public class SingleThreadWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {      //处理多个请求
            Socket connection = socket.accept();//通过80端口接收到HTTP请求
            handleRequest(connection); //处理请求
        }
    }

    private static void handleRequest(Socket connection) {
        // 请求处理
    }
}

通过80端口接收到HTTP请求很简单且理论上是正确的,但在实际生产环境中的执行性能很糟糕,因为它每次只能处理一个请求。主线程在接受连接与处理相关请求等操作之间不断交替运行。

在Web请求的处理中包含了一组不同的运算与I/O操作。服务器必须处理套接字I/O以读取请求和写回响应,这些操作通常会由于网络阻塞或连通性问题而被阻塞。

在服务器应用程序中,串行处理机制通常都无法提供高吞吐率或快速响应性。

6.1.2 显式地为任务创建线程(Explicitly 【明确地】Creating Threads for Tasks)

通过为每个请求创建一个新的线程来提供服务,从而实现更高的响应性。

//     6-2   在Web服务器中为每个请求启动一个新的线程(不要这么做)
public 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();//线程启动
        }
    }

    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}

ThreadPerTaskWebServer的结构与串行版本类似——主线程仍然不断地交替执行“接受外部请求”与“分发请求”等操作。区别在于,对于每个连接,主循环都创建一个新线程来处理请求,而不是在主循环中处理。

由此得出3个主要结论:
①任务处理过程从主线程中分离出来,使得主循环能够更快地等待下个到来的连接。这使得程序在完成前面的请求之前可以接受新的请求,从而提高响应性。
②任务可以并行处理 ,从而同时服务多个请求。如果有多个处理器,或者任务由于某个原因被阻塞,例如等待I/O完成,获取锁或者资源可用性等,程序的吞吐率将得到提高。
③任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。

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

6.1.3 无限制创建线程(Unbounded Thread Creation)的不足

“为每个任务分配一个线程”存在一些缺陷,特别是当需要创建大量的线程时:
①线程生命周期的开销非常高。

②资源消耗。
活跃的线程会消耗系统资源,尤其是内存。

③稳定性
在可创建线程的数量上存在一个限制。

在一定的范围内,增加线程可以提高系统的吞吐率,但如果超出了这个范围,在创建更多的线程只会降低程序的执行速度,并且如果过多地创建一个线程,那么整个应用程序将崩溃。

要想避免这种危险,就应该对应用程序可以创建的线程数量进行限制,并且全面地测试应用程序,从而确保在线程数量达到限制时,程序也不会耗尽资源。

“为每个任务分配一个线程”问题在于,没有限制可创建线程的数量,只限制了远程用户提交HTTP请求的速率。


6.2 Executor框架(Framework)

任务是一组逻辑工作单元,而线程则是使任务异步执行的机制。

把所有任务放在单个线程中,以及将每个任务放在各自的线程中执行,这两种方式都存在一些严格限制:串行执行的问题在于其糟糕的响应性和吞吐率,而“为每个任务分配一个线程”问题在于资源管理的复杂性。

线程池简化了线程的管理任务,并且java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分。在Java类库中,任务执行的主要抽象不是Thread,而是Executor。

//                   Excutor接口
public interface Executor{
    void execute(Runnable command);
}

Executor是个简单的接口,为灵活且强大的异步执行任务框架提供了基础,该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程执行过程解耦开来,并用Runnable来表示任务。Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。

Executor基于生产者-消费者模式,提交任务的操作相当于生成者(生成待完成的工作单元),执行任务的线程则相当于消费者(执行完这些工作单元)。

如果要在程序中实现一个生产者-消费者的设计,那么最简答的方式就是使用Executor。(而我们之前的章节中使用的是阻塞队列来实现的,利用其take和put方法)

6.2.1 基于Executor的Web服务器

TaskExecutionWebServer 代替了硬编码的线程创建过程。

使用了标准的Executor实现,即一个固定长度的线程池,可以容纳100个线程

//      6-4    基于线程池的Web服务器
public class TaskExecutionWebServer {
   
     private static final int NTHREADS=100;
     private static final Executor exec=
             Executors.newFixedThreadPool(NTHREADS); //创建了一个固定长度的线程池,可以容纳100个线程

     public static void main(String[] args)throws IOException{
         ServerSocket socket=new ServerSocket(80);//创建一个Server Socket绑定到80端口自
         while(true){
             final Socket connection=socket.accept();
             Runnable task=new Runnable(){
                 public void run(){
                     handleRequest(connection);
                 }
             };
             exec.execute(task);//将任务提交到工作队列中,在某个时刻被工作线程取出并执行
         }
     }
     private static void handleRequest(Socket connection) {
         //处理请求
     }
}

通过使用Executor,将请求处理任务的提交与任务的实际执行解耦开来,并且只需采用另一种不同的Executor实现,就可以改变服务器的行为。

通常,Executor的配置是一次性,因此可以在部署阶段可以完成,而提交任务的代码却会不断地扩散到整个程序中,增加了修改的难度。

我们可以将TaskExecutionWebServer修改为类似ThreadPerTaskWebServer的行为,只需使用一个为每个请求都创建新线程的Executor。

//     6-5   为每个请求启动一个新线程的Executor
public class ThreadPerTaskExecutor implements Executor {
   
    public void execute(Runnable r) {
        new Thread(r).start();
    };
}

同样

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值