首页 > 代码库 > 《Java并发编程实战》(五)---- 任务执行

《Java并发编程实战》(五)---- 任务执行

一,在线程中执行任务

1,无限创建线程的不足:

  • 线程生命周期的开销非常高。线程的创建过程需要时间,这就延迟了请求的处理,并且需要JVM和操作系统提供一些辅助操作。
  • 资源消耗。如果可运行线程数量多于可用处理器的数量,那么有些线程会闲置就会占用许多内存,如果大量线程在竞争CPU还会产生其他的性能消耗。
  • 稳定性。在可创建线程的数量上有一个阈值,这个阈值随着平台不同而不同,并且受多个因素制约,包括JVM的启动参数、Thread构造函数中请求的栈大小,以及底层操作系统对线程的限制等。如果超过这个限制,就很可能有OOM异常。

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

2,Executor框架

串行执行的问题在于其糟糕的响应性和吞吐量,而“为每个任务都分配一个线程”的问题在于资源管理的复杂性。所以就有了线程池,线程池简化了线程的管理工作,并且java.util.concurrent提供了一种灵活的线程池作为Executor框架的一部分。在java类库中,任务执行的不是Thread,而是Executor:

public interface Executor {
    void execute(Runnable command);
}

1,基于Executor的Web服务器

class TaskExecutorWebServer {
    private static final int NTHREAD = 100;
    private static final Executor exe = Executors.newFixedThreadPool(NTHREAD);

    public static void main(String[] args) {
        ServerSocket socket = new ServerSocket(80);
        while(true) {
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            };
            exec.execute(task);
        }
    }
}

在TaskExecutionWebServer中,通过使用Executor,将请求处理任务的提交与任务的实际执行解耦开来。

也可以很容易地将上例修改为ThreadPerTaskWebServer的行为,只需要使用一个为每个请求都创建新线程的Executor。

public class ThreadPerTaskExecutor implements Executor {
    public void execute(Runnable r) {
        new Thread(r).start();
    }
}

还可以编写一个Executor使ThreadPerTaskExecutor的行为类似于单线程的行为:

public class WithinThreadExecutor implements Executor {
    public void execute(Runnable r) {
        r.run();
    }
}

***************************************************

每当看到下面形式的代码时,并且希望获得一种更灵活的执行策略时,考虑使用Executor来代替Thread:

new Thread(runnable).start();

***************************************************

3, 线程池

线程池是指管理一组相同工作线程的资源池。线程池是与工作队列密切相关的,其中在工作队列中保存了所有等待执行的任务。工作线程的任务很简单: 从工作队列中获取一个任务,执行任务,然后返回线程池并等待下一个任务。

“在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以减少在线程创建与销毁的开销。另一个好处是请求到来时,不会再因为要等待线程创建而延迟,也就提高了响应性。通过适当调整线程池的大小,可以创建足够多的线程以便处理器保持忙碌状态,同时还可以防止过多线程互相竞争资源而使应用程序耗尽内存。

类库提供了一个灵活的线程池以及一些有用的默认配置。可以通过调用Executors中的静态工厂方法之一来创建一个线程池:

  • newFixedThreadPool:创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化。
  • newCachedThreadPool:创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
  • newSingleThreadExecutor:是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。它能确保依照任务在队列中的顺序来串行执行。
  • newScheduledThreadPool:创建了一个固定长度的线程池,而且以延迟或定时的方式执行任务,类似于Timer。

4,Executor的生命周期

由于Executor以异步方式来执行任务,因此在任何时刻,之前提交任务的状态不是立即可见的。有些任务可能已经完成,有些可能正在运行,而其他的任务可能在队列中等待执行。当关闭应用程序时,可能采用平缓的方式(完成所有已经启动的任务,并且不再接受任何新的任务),也可能采用粗暴方式(直接所有都关掉)。Executor视为应用程序提供服务的,因此它们也是可关闭的,并把在关闭操作中受影响的任务的状态返回给应用程序。

为了解决执行任务的生命周期问题,ExecutorService接口扩展了Executor,添加了一些用于生命周期管理的方法:

public interface ExecutorService extends Executor {
    void shutdown();
    List<Runnable> shutDownNow();
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
    ...
}

ExectuorService的生命周期有三种状态:

《Java并发编程实战》(五)---- 任务执行