Spring 中使用 Java 5.0 Executor

Java 5.0 新增了一个并发工具包 java.util.concurrent,该工具包由 DougLea 设计并作为 JSR-166 添加到 Java 5.0 中。这是一个非常流行的并发工具包。它提供了功能强大的、高层次的线程构造器,包含执行器、线程任务框架、线程安全队列、计时器、锁(包含原子级别的锁)和其他一些同步的基本类型。

执行器 Executor 是并发工具包中一个重要的类,它对 Runnable 实例的执行进行了抽象,实现者可以提供具体的实现,如简单地以一个线程来运行 Runnable,或者通过一个线程池为 Runnable 提供共享线程。

因为 Executor 是 Java 5.0 新增的类,所以 Java 5.0 提供的实现类大多拥有线程池的内在支持。Spring 为 Executor 处理引入了一个新的抽象层,以便将线程池引入 Java 1.3 和 Java 1.4 环境中,同时屏蔽掉 Java 1.3、1.4、5.0 及 JavaEE 环境中线程池实现的差异。

1.了解 Java 5.0 的 Executor

java.util.concurrent.Executor 接口的主要目的是将“任务提交”和“任务执行”分离解耦。该接口定义了任务提交的方法,实现者可以提供不同的任务执行机制,指定不同的线程使用规则和调度方案。

Executor 只有一个方法:void execute(Runnable command),它接收任何实现了 Runnable 的实例,这个实例代表一个待执行的任务。可以使用如下代码提交任务:

Executor executor  = new Executo();
executor.executor(new RunnableTask1()); //提交一个任务
executor.executor(new RunnableTask2()); //提交另一个任务

Executor 本身并没有要求实现者以何种方式执行这些任务,一个简单的实现类甚至可以在提交任务时,立即在主线程中执行它们。下面是一个 Executor 最简单的实现:

public class SimpleExecutor implements Executor{
    public void execute(Runnable r){
        r.run(); //①在提交时直接执行任务
    }
}

但在更多的情况下,需要在一个异步线程中执行任务,而非在主线程中执行任务。下面是一个稍微有意义的实现,它为每个任务开启一个新的执行线程。

public class ThreadPerTaskExecutor implements Executor{
    public void execute(Runnable r){
        new Thread(r).start();  //①在新的执行线程中执行任务
    }
}

但以上这些粗陋的实现并没有多大的实际应用价值,这也不符合 Executor 的设计初衷。真正有意义的实现需要引入线程、列队、调度等机制,这样的执行器才更贴近现实的需求。

Executor 接口还有两个子接口:ExecutorService 和 ScheduledExecutorService。ExecutorService 添加了结束任务的管理方法,此外,在提交任务时还可以获取一个 Future 实例,以便通过这个实例跟踪异步任务的执行情况。而 ScheduledExecutorService 可以对任务进行调度,如指定执行的延迟时间及执行的周期。如 ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) 方法将安排一个任务在延迟一段时间后执行。

Java 5.0 本身提供的 ThreadPoolExecutor 类实现了 Executor 和 ExecutorService 这两个接口,它使用一个线程池对提交的任务进行调度。如果需要处理数量巨大的短小并发任务(如 Web 服务器、数据库服务器、邮件服务器之类的应用程序需要处理来自远程的大量短小的任务),则采用线程池可以带来明显的好处。为每个请求创建一个新线程的开销很大,对于短小的任务,线程创建和销毁的时间可能比处理实际任务的时间还要长。除此以外,活动的线程需要消耗资源,过多的线程将导致大量的内存占用。线程池能让多个任务重用同一个线程,线程创建的开销被分摊到多个任务上。此外,由于在任务到达时线程已经存在,所以也消除了任务执行时因创建线程所带来的延迟。可以通过适当地调整线程池中的参数,当任务的数目超过某个阈值时,强制新任务排队等待,直到获得一个线程来处理,从而防止资源无限制消耗而引发的系统问题。

ThreadPoolExecutor 的子类 ScheduledThreadPoolExecutor 实现了 ScheduledExecutorService 接口,添加了对任务的调度功能,如指定延迟一小段时间后执行任务,让任务周期性执行。该类明显优于 Java 1.3 中的 Timer,因为它通过内建的线程池让每个任务在独立的执行线程中执行,而非让所有任务在单一的背景线程中执行。Timer 经常出现的时间漂移、任务挤压等问题基本上得到了规避。

在 java.util.concurrent 中为创建这些接口实例提供了一个综合性的工厂类 Executors,它拥有以下众多方便的静态工厂方法。

(1)public static ExecutorService newFixedThreadPool(int nThreads):创建一个线程池,重复使用一组固定的线程执行任务。

(2)public static ExecutorService newCachedThreadPool():线程池是动态的,不够用时将创建新的线程,长时间不用的线程将被收回。

(3)public static ScheduledExecutorService newScheduledThreadPool(int corePooISize,ThreadFactory threadFactory):创建一个线程池,可在指定延迟后执行或者定期执行。

来看一个具体的例子,如下面代码所示。

public class ExecutorExample {
    private Executor executor;//①声明一个执行器
    public void setExecutor(TaskExecutor executor) {
        this.executor = executor;
    }
    public void executeTasks() {//②用执行器执行多个任务
        for (int i = 0; i < 6; i++) {
            executor.execute(new SimpleTask("task" + i));
        }
    }
    public static void main(String[] args) {
        ExecutorExample ee = new ExecutorExample();
        ee.setExecutor(Executors.newFixedThreadPool(3));//③通过工厂类创建一个带3个线程的固定线程池的执行器
        ee.executeTasks();
    }
}
class SimpleTask implements Runnable {//④任务类
    private String taskName;
    public SimpleTask(String taskName) {
        this.taskName = taskName;
    }
    public void run() {
        System.out.println("do " + taskName + "... in Thread:"
                + Thread.currentThread().getId());
    }
}

运行以上代码,输出以下信息

do task0... in Thread:7
do task1... in Thread:8
do task2... in Thread:9
do task3... in Thread:7
do task5... in Thread:9
do task4... in Thread:8

可见,这6个任务共享了线程池中的3个线程。由于 ExecutorService 用线程池中的3个线程服务于提交的任务,从而避免了为每个任务创建独立线程的代价,具有更高的运行性能。

 

2.Spring 对 Executor 所提供的抽象

Spring 的 org.springframework.core.task.TaskExecutor 接口等同于 java.util.concurrent.Executor 接口。该接口和 Java 5.0 的 Executor 接口拥有相同的 execute(Runnable task) 方法。TaskExecutor 拥有一个 SchedulingTaskExecutor 子接口,新增了任务调度规则定制的功能。

在 Spring 发行包中预定义了一些实现,它们可以满足大部分应用要求,一般情况下,用户不必自行编写实现类。下面是 TaskExecutor 的实现类。

SyncTaskExecutor:位于 org.springframework.core.task 包中,实现了 TaskExecutor 接口。这个实现不会异步执行任务:相反,每次调用都在发起调用的主线程中执行。

下面是 SchedulingTaskExecutor 的实现类。

(1)SimpleAsyncTaskExecutor:位于 org.springframework.core.task 包中。该类没有使用线程池,在每次执行任务时都创建一个新线程。但是,它依然支持对并发总数设限,当超过并发总数限制时,阻塞新的任务直到有可用的资源。

(2)ConcurrentTaskExecutor:位于 org.springframework.scheduling.concurrent 包中。该类是 Java 5.0 的 Executor 的适配器,以便将 Java 5.0 的 Executor当作 Spring 的 TaskExecutor 使用。

(3)SimpleThreadPoolTaskExecutor:位于org.springframework.scheduling.quartz 包中。该类实际上是继承于 Quartz 的 SimpleThreadPool 类的子类,它将监听 Spring 的生命周期回调。当用户有线程池,需要在和非 Quartz 组件中共用时,该类可以发挥它的用处。

(4)ThreadPoolTaskExecutor:位于  org.springframework.scheduling.concurrent 包中。该类只能在 Java 5.0 中使用,它暴露了一些属性,方便在 Spring 中配置一个 java.util.concurrent.ThreadPoolExecutor,并把它包装成 TaskExecutor。

(5)TimerTaskExecutor:位于 org.springframework.scheduling.timer 包中。该类使用一个 Timer 作为其后台实现。

上面代码中的 ExecutorExample 只能在Java 5.0 下运行,如果用 Spring 的 TaskExecutor 替换①处的 Executor,当程序需部署到低版本的 Java 环境中时,仅需要选择一个合适的实现就可以了。

public class ExecutorExample {
    private TaskExecutor executor;//①使用Spring的TaskExecutor替换Java 5.0的Executor
    public void setExecutor(TaskExecutor executor) {
        this.executor = executor;
    }
    public void executeTasks() {//②用执行器执行多个任务
        for (int i = 0; i < 6; i++) {
            executor.execute(new SimpleTask("task" + i));
        }
    }
    public static void main(String[] args) {
        ExecutorExample ee = new ExecutorExample();
        ee.setExecutor(new SimpleAsyncTaskExecutor());//②使用Spring的TaskExecutor实现类
        ee.executeTasks();
    }
}
class SimpleTask implements Runnable {
    ...
}

如果将 ExecutorExample 配置成一个 Bean,通过注入的方式提供 executor 属性,就可以方便地选用不同的实现版本。如果是在 Java 5.0 版本中,则用户可以选用 ThreadPoolTaskExecutor;而在 JDK 低版本中则可以使用 SimpleAsyncTaskExecutor。这样,程序就可以在不同的 JDK 版本中进行移植。

 

posted @ 2019-08-18 15:16  认真对待世界的小白  阅读(786)  评论(0编辑  收藏  举报