Java™ 教程(执行器)

35岁危机
• 阅读 1676

执行器

在前面的所有示例中,由新的线程(由其Runnable对象定义)和线程本身(由Thread对象定义)完成的任务之间存在紧密的联系,这适用于小型应用程序,但在大型应用程序中,将线程管理和创建与应用程序的其余部分分开是有意义的,封装这些函数的对象称为执行器,以下小节详细描述了执行器。

  • 执行器接口定义三个执行器对象类型。
  • 线程池是最常见的执行器实现类型。
  • Fork/Join是一个利用多个处理器的框架(JDK 7中的新增功能)。

执行器接口

java.util.concurrent包定义了三个执行器接口:

  • Executor,一个支持启动新任务的简单接口。
  • ExecutorServiceExecutor的子接口,它添加了有助于管理生命周期的功能,包括单个任务和执行器本身。
  • ScheduledExecutorServiceExecutorService的子接口,支持将来和/或定期执行任务。

通常,引用执行器对象的变量被声明为这三种接口类型之一,而不是执行器类类型。

Executor接口

Executor接口提供单个方法execute,旨在成为常见线程创建语法的替代方法,如果rRunnable对象,并且eExecutor对象,则可以替换

(new Thread(r)).start();

e.execute(r);

但是,execute的定义不太具体,低级别语法创建一个新线程并立即启动它,根据Executor实现,execute可能会做同样的事情,但更有可能使用现有的工作线程来运行r,或者将r放在队列中以等待工作线程变为可用(我们将在线程池的部分中描述工作线程)。

java.util.concurrent中的执行器实现旨在充分利用更高级的ExecutorServiceScheduledExecutorService接口,尽管它们也可以与基本Executor接口一起使用。

ExecutorService接口

ExecutorService接口使用类似但更通用的submit方法补充execute,与execute一样,submit接受Runnable对象,但也接受Callable对象,这允许任务返回一个值。submit方法返回一个Future对象,该对象用于检索Callable返回值并管理CallableRunnable任务的状态。

ExecutorService还提供了提交大量Callable对象的方法,最后,ExecutorService提供了许多用于管理执行器关闭的方法,为了支持立即关闭,任务应该正确处理中断

ScheduledExecutorService接口

ScheduledExecutorService接口使用schedule补充其父级ExecutorService的方法,在指定的延迟后执行RunnableCallable任务,此外,接口定义了scheduleAtFixedRatescheduleWithFixedDelay,它们以定义的间隔重复执行指定的任务。

线程池

java.util.concurrent中的大多数执行器实现都使用由工作线程组成的线程池,这种线程与它执行的RunnableCallable任务分开存在,通常用于执行多个任务。

使用工作线程可以最小化由于创建线程而带来的开销,线程对象使用大量内存,在大型应用程序中,分配和释放许多线程对象会产生大量的内存管理开销。

一种常见类型的线程池是固定线程池,这种类型的池始终具有指定数量的线程,如果一个线程在它仍在使用时以某种方式被终止,它将自动被一个新线程替换,任务通过内部队列提交到池中,当活动任务多于线程时,该队列将保存额外的任务。

固定线程池的一个重要优点是使用它的应用程序可以优雅地降级,要理解这一点,请考虑一个Web服务器应用程序,其中每个HTTP请求都由一个单独的线程处理。如果应用程序只是为每个新的HTTP请求创建一个新线程,并且系统接收的请求数量超过了可以立即处理的数量,当所有这些线程的开销超过系统容量时,应用程序将突然停止响应所有请求。由于可以创建的线程数量有限制,应用程序不会像HTTP请求进入时那样快地为它们提供服务,而是以系统能够承受的最快速度为它们提供服务。

创建使用固定线程池的执行器的一种简单方法是在java.util.concurrent.Executors中调用newFixedThreadPool工厂方法,该类还提供以下工厂方法:

  • newCachedThreadPool方法使用可扩展线程池创建执行器,此执行器适用于启动许多短期任务的应用程序。
  • newSingleThreadExecutor方法创建一次执行单个任务的执行器。
  • 有几个工厂方法是上述执行器的ScheduledExecutorService版本。

如果上述工厂方法提供的执行器均无法满足你的需求,构造java.util.concurrent.ThreadPoolExecutorjava.util.concurrent.ScheduledThreadPoolExecutor的实例将为你提供额外选项。

Fork/Join

fork/join框架是ExecutorService接口的一个实现,可帮助你利用多个处理器,它专为可以递归分解成小块的工作而设计,目标是使用所有可用的处理能力来增强应用程序的性能。

与任何ExecutorService实现一样,fork/join框架将任务分配给线程池中的工作线程,fork/join框架是不同的,因为它使用了工作窃取算法,没有事情可做的工作线程可以从仍然忙碌的其他线程中窃取任务。

fork/join框架的中心是ForkJoinPool类,它是AbstractExecutorService类的扩展,ForkJoinPool实现了核心工作窃取算法,可以执行ForkJoinTask进程。

基础用法

使用fork/join框架的第一步是编写执行工作片段的代码,你的代码应类似于以下伪代码:

if (我的工作部分足够小)
  直接做这项工作
else
  把我的工作分成两块
  调用这两块并等待结果

将此代码包装在ForkJoinTask子类中,通常使用其更专业的类型之一,RecursiveTask(可以返回结果)或RecursiveAction

ForkJoinTask子类准备就绪后,创建表示要完成的所有工作的对象,并将其传递给ForkJoinPool实例的invoke()方法。

模糊清晰度

为了帮助你了解fork/join框架的工作原理,请考虑以下示例,假设你想模糊图像,原始源图像由整数数组表示,其中每个整数包含单个像素的颜色值,模糊的目标图像也由与源相同大小的整数数组表示。

通过一次一个像素地处理源数组来完成模糊,将每个像素与其周围像素进行平均(对红色、绿色和蓝色组件进行平均),并将结果放置在目标数组中,由于图像是大型数组,因此此过程可能需要很长时间,通过使用fork/join框架实现的算法,你可以利用多处理器系统上的并发处理,这是一个可能的实现:

public class ForkBlur extends RecursiveAction {
    private int[] mSource;
    private int mStart;
    private int mLength;
    private int[] mDestination;
  
    // Processing window size; should be odd.
    private int mBlurWidth = 15;
  
    public ForkBlur(int[] src, int start, int length, int[] dst) {
        mSource = src;
        mStart = start;
        mLength = length;
        mDestination = dst;
    }

    protected void computeDirectly() {
        int sidePixels = (mBlurWidth - 1) / 2;
        for (int index = mStart; index < mStart + mLength; index++) {
            // Calculate average.
            float rt = 0, gt = 0, bt = 0;
            for (int mi = -sidePixels; mi <= sidePixels; mi++) {
                int mindex = Math.min(Math.max(mi + index, 0),
                                    mSource.length - 1);
                int pixel = mSource[mindex];
                rt += (float)((pixel & 0x00ff0000) >> 16)
                      / mBlurWidth;
                gt += (float)((pixel & 0x0000ff00) >>  8)
                      / mBlurWidth;
                bt += (float)((pixel & 0x000000ff) >>  0)
                      / mBlurWidth;
            }
          
            // Reassemble destination pixel.
            int dpixel = (0xff000000     ) |
                   (((int)rt) << 16) |
                   (((int)gt) <<  8) |
                   (((int)bt) <<  0);
            mDestination[index] = dpixel;
        }
    }
  
  ...

现在,你实现抽象的compute()方法,该方法可以直接执行模糊或将其拆分为两个较小的任务,简单的数组长度阈值有助于确定是执行还是拆分工作。

protected static int sThreshold = 100000;

protected void compute() {
    if (mLength < sThreshold) {
        computeDirectly();
        return;
    }
    
    int split = mLength / 2;
    
    invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
              new ForkBlur(mSource, mStart + split, mLength - split,
                           mDestination));
}

如果以前的方法在RecursiveAction类的子类中,那么将任务设置为在ForkJoinPool中运行是很简单的,涉及以下步骤:

  1. 创建一个代表要完成的所有工作的任务。

    // source image pixels are in src
    // destination image pixels are in dst
    ForkBlur fb = new ForkBlur(src, 0, src.length, dst);
  2. 创建将运行任务的ForkJoinPool

    ForkJoinPool pool = new ForkJoinPool();
  3. 运行任务。

    pool.invoke(fb);

有关完整源代码(包括创建目标图像文件的一些额外代码),请参阅ForkBlur示例。

标准实现

除了使用fork/join框架来实现在多处理器系统上同时执行任务的自定义算法(例如ForkBlur.java示例),Java SE中已经使用fork/join框架实现了一些通常有用的功能,在Java SE 8中引入的一种这样的实现被java.util.Arrays类用于其parallelSort()方法,这些方法类似于sort(),但通过fork/join框架利用并发性。在多处理器系统上运行时,大型数组的并行排序比顺序排序更快,但是,这些方法如何利用fork/join框架超出了Java教程的范围,有关此信息,请参阅Java API文档。

fork/join框架的另一个实现由java.util.streams包中的方法使用,这是Project Lambda计划用于Java SE 8版本的一部分,有关更多信息,请参阅Lambda表达式部分。


上一篇:Lock对象
下一篇:原子变量
点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java并发程序和共享对象实用策略
java并发程序和共享对象实用策略在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:1.线程封闭2.只读共享。共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象3.线程安全共享。线程安全地对象在器内部实现同步。4.保护对象。被保护的对象只能通过持有特定的锁
Wesley13 Wesley13
3年前
java多线程(待完善)
1、小型系统//线程完成的任务(Runnable对象)和线程对象(Thread)之间紧密相连classAimplementsRunnable{publicvoidrun(){//业务逻辑}}
lucien-ma lucien-ma
4年前
什么是线程?什么是进程?
Java多线程基础进程和线程的概念应用程序是静态的概念,进程和线程是动态概念,有创建就有销毁,存在也是暂时的,不是永久性的。进程与线程的区别在于进程在运行时拥有独立的内存空间(每个进程所占有的内存都是独立的)多个线程是共享内存空间的,但是每个线程的执行时相互独立的,同时线程必须依赖于进程才能执行,单独的线程是无法执行的,由进程来控制多个线程的执行。
Wesley13 Wesley13
3年前
Java 并发编程:任务执行器 Executor 接口
任务执行器(Executor)是一个接口,位于java.util.concurrent包下,它的作用主要是为我们提供任务与执行机制(包括线程使用和调度细节)之间的解耦。比如我们定义了一个任务,我们是通过线程池来执行该任务,还是直接创线程来执行该任务呢?通过Executor就能为任务提供不同的执行机制。执行器的实现方式各种各样,常见的包括同步执行器、一对一执行
Wesley13 Wesley13
3年前
Java线程知识深入解析(2)
多线程程序对于多线程的好处这就不多说了。但是,它同样也带来了某些新的麻烦。只要在设计程序时特别小心留意,克服这些麻烦并不算太困难。(1)同步线程许多线程在执行中必须考虑与其他线程之间共享数据或协调执行状态。这就需要同步机制。在Java中每个对象都有一把锁与之对应。但Java不提供单独的lock和unlock操作。它由高层的结构隐
Stella981 Stella981
3年前
Executor框架详解
任务是一组逻辑工作单元,线程是使任务异步执行的机制。下面分析两种通过创建线程来执行任务的策略。1将所有任务放在单个任务中串行执行;2为每个任务创建单独的线程来执行实际上两种方式都存在一些严重的缺陷。串行执行的问题在于其糟糕的响应和吞吐量;而为每个任务单独创建线程的问题在于资源管理的复杂性,容易造成资源的浪费和过度消耗,影响系统的稳定性。为了提
Wesley13 Wesley13
3年前
Java总结:Java多线程
多线程作为Java中很重要的一个知识点,在此还是有必要总结一下的。Java给多线程编程提供了内置的支持。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。这里定义和线程相关的另一个术语进程:一个进程包括由操作系统分配的内存空间,
Stella981 Stella981
3年前
GitHub上的今年第一本《Java异步编程实战》美团T9亲荐,太赞了
异步编程是可以让程序并行运行的一种手段,可以让程序中的一个工作单元与主应用程序线程分开独立运行,进而提高应用程序的性能和响应能力等。虽然Java为不同技术域提供了相应的异步编程技术,但是这些异步编程技术被散落到不同技术域的
Stella981 Stella981
3年前
Spring 5 中文解析核心篇
一个典型的企业应用不是由一个简单的对象(在Spring中叫bean)组成。即使是最简单的应用程序,也有一些对象协同工作,以呈现最终用户视为一致的应用程序。(备注:相当于所有的bean一起协同工作对于用户是无感知的)。下一部分将说明如何从定义多个独立的Bean对象协作去实现应用程序的目标。1.4.1依赖注入依赖注入是从工厂方法构造或
Wesley13 Wesley13
3年前
Java核心技术卷一基础知识
第14章多线程本章内容:什么是线程中断线程线程状态线程属性同步阻塞队列线程安全的集合Collable与Future执行器同步器线程与Swing1.通
小万哥 小万哥
1年前
掌握 Spring IoC 容器与 Bean 作用域:详解 singleton 与 prototype 的使用与配置
在您的应用程序中,由SpringIoC容器管理的形成其核心的对象被称为"bean"。一个bean是由SpringIoC容器实例化、组装和管理的对象。这些bean是通过您提供给容器的配置元数据创建的,例如,在前面章节中已经看到的XML定义。Bean定义包含了