Executor框架

1.在Java 5之后并发编程引入了一堆新的启动、调度和管理线程的API。Executor框架便是Java 5中引入的,其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象。但是如果是在Executor中启动的话,就不会造成整个问题。

  this逃逸问题详细的解答:

是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误, 因此应该避免this逃逸的发生.this逃逸经常发生在构造函数中启动线程或注册监听器时。

//注意,Start()是在构造器中调用的。这个实例相当的简单,因此可能是安全的。但是应该注意到
//在构造器重启动线程可能会变得很有问题。因为另外一个任务可能会在构造器结束以前就开始执行了
//这意味着该任务能够访问处于不稳定状态的对象。这是优选Executor而不是显式地创建Thread对象的
//另外一个很重要的原因。
public class SelfManaged implements  Runnable {
    private int countDown=5;
    private Thread t = new Thread(this);//Thread中传入的就是一个Runnable对象。

    public SelfManaged() {
        t.start();
    }

    public String toString(){
        return  Thread.currentThread().getName()+"("+countDown+")";
    }
    @Override
    public void run() {
        while (true) {//必须要有结束的条件。
            System.out.println(this);
            if (--countDown == 0) {
                return;
            }
        }

    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new SelfManaged();
        }

    }
}

Executor框架_第1张图片

Executor框架_第2张图片


2.Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

3.使用Executor的一般的步骤:

 Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

    ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当素有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。

4.使用Executors这个类来创建不同的线程池之间的区别。

    Executors提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。   

    public static ExecutorService newFixedThreadPool(int nThreads)

     创建固定数目线程的线程池。有了FixedThreadPool,你就可以一次性地执行代价高昂的线程的分配,因而也就可以限制线程的数量了。这可以节省时间,因为你不用为每一个任务都固定地付出创建线程的开销。线程在线程池中是可以重复使用的。但是在这里的话,线程是不可以有所谓的等待的时间的的,当一个线程执行完任务以后,如果没有任务让这个线程池中的线程执行的话,那么这个线程就会被回收。这和 newCachedThreadPool是不同的,newCachedThreadPool是不同的,后者可以有60秒的等待的时间才会被回收。俩者的区别就是有或者没有无IDLEDE的区别。在事件驱动的系统中,需要线程的事件处理器,通过直接从线程池中获取线程,也就可以如你所愿的尽快地得到服务。因为你不会滥用可获得的资源,因为FixThreadPool使用的Thread对象的数量是有限的。

    public static ExecutorService newCachedThreadPool()

    创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线   程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。如果这个线程有60秒没有被使用了,那么这个线程就会被移除。一个线程在执行完任务以后,是不会立刻被回收的。默认情况下,只有在等待了60秒,这个线程还是闲置的话,那么这个这个线程就会回收。这是要注意的。

    public static ExecutorService newSingleThreadExecutor()

    创建一个单线程化的Executor。SingleThreadExecutor就像是线程数量是1的FixedThreadPool.这对于你希望在另外一个线程中连续运行的任何事物(长期存活的任务)来说,都是很有用的。例如监听进入的套接字连接的服务。它对于希望在线程中运行的短任务也是同样的方便,例如更新本地或者远程日志的小任务,或者是事件分发线程。

   如果向SingleThreadExecutor提交了多个任务,那么这些任务将排队,每一个任务都会在下一个任务开始运行之前结束,所有的任务都将使用相同的线程。你可以看到每个任务都是按照它们被提交的顺序,并且是在下一个任务开始之前完成的。因此SingleThreadExecutor会序列化所有提交给它的任务,并且会维护它自己的悬挂的任务队列。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

  总体的概括如下:

  




newCachedThreadPool()                                                                                                                                         

-缓存型池子,先查看池中有没有以前建立的线程,如果有,就 reuse.如果没有,就建一个新的线程加入池中
-缓存型池子通常用于执行一些生存期很短的异步型任务
 因此在一些面向连接的daemon型SERVER中用得不多。但对于生存期短的异步任务,它是Executor的首选。
-能reuse的线程,必须是timeout IDLE内的池中线程,缺省     timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。
  注意,放入CachedThreadPool的线程不必担心其结束,超过TIMEOUT不活动,其会自动被终止。



newFixedThreadPool(int)                                                      

-newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程

-其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待直到当前的线程中某个线程终止直接被移出池子
-和cacheThreadPool不同,FixedThreadPool没有IDLE机制(可能也有,但既然文档没提,肯定非常长,类似依赖上层的TCP或UDP IDLE机制之类的),所以FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器
-从方法的源代码看,cache池和fixed 池调用的是同一个底层 池,只不过参数不同:
fixed池线程数固定,并且是0秒IDLE(无IDLE)    
cache池线程数支持0-Integer.MAX_VALUE(显然完全没考虑主机的资源承受能力),60秒IDLE  


newScheduledThreadPool(int)

-调度型线程池
-这个池子里的线程可以按schedule依次delay执行,或周期执行

SingleThreadExecutor()

-单例线程,任意时间池中只能有一个线程
-用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)

Executor框架_第3张图片


一般来说,CachedThreadPool是我们执行异步任务的首选,但是也应该考虑在产生线程的代码中使用FixedThreadPool.CachedThreadPool在程序的执行过程当中,通常会创建与所需数量相同的线程,然后在它回收旧线程的时候,停止创建新线程。因此它是合理的Executor的首选。只有当这种方式会引发问题的时候,你才需要切换到FixedThreadPool;

5.什么事异步任务,我们应该怎么理解异步任务:

  

什么是异步?为什么要用它?

异步编程提供了一个非阻塞的,事件驱动的编程模型。 这种编程模型利用系统中多核执行任务来提供并行,因此提供了应用的吞吐率。此处吞吐率是指在单位时间内所做任务的数量。 在这种编程方式下, 一个工作单元将独立于主应用线程而执行, 并且会将它的状态通知调用线程:成功,处理中或者失败。

我们需要异步来消除阻塞模型。其实异步编程模型可以使用同样的线程来处理多个请求, 这些请求不会阻塞这个线程。想象一个应用正在使用的线程正在执行任务, 然后等待任务完成才进行下一步。 log框架就是一个很好的例子:典型地你想将异常和错误日志记录到一个目标中, 比如文件,数据库或者其它类似地方。你不会让你的程序等待日志写完才执行,否则程序的响应就会受到影响。 相反,如果对log框架的调用是异步地,应用就可以并发执行其它任务而无需等待。这是一个非阻塞执行的例子。

为了在Java中实现异步,你需要使用Future 和 FutureTask, 它们位于java.util.concurrent包下. Future是一个接口而FutureTask是它的一个实现类。实际上,如果在你的代码中使用Future, 你的异步任务会立即执行, 并且调用线程可以得到结果promise

下面的代码片段定义了一个包含两个方法的接口。 一个是同步方法,另外一个是异步方法。

1
2
3
4
5
6
7
import java.util.concurrent.Future;
public interface IDataManager {
    // synchronous method
    public String getDataSynchronously();
    // asynchronous method
    public Future getDataAsynchronously();
}

值得注意的是回调模型的弊端就是当回调嵌套时很麻烦。

该做和不该做的

为了方便测试, 你应该在代码中将功能从多线程中隔离出来。当在Java中编写异步代码时,你应该遵循异步模型,这样调用线程就不会被阻塞。
注意构造函数不能是异步的,你不应该在构造函数中调用异步方法。当任务互相不依赖时异步方式尤其有用。当调用任务依赖被调用任务时不应该使用异步(译者按:这对异步来说无意义,因为业务上调用线程被阻塞了).

你应该在异步方法中处理异常. 你不应该为长时间的task实现异常. 一个长时间运行的任务,如果异步执行的话, 可能会比同步执行耗费更长的时间, 因为运行时要为异步执行的方法执行线程上下文的切换, 线程状态的存储等. 你也应该注意同步的异常和异步的异常有所不同。 同步异常暗示 每次程序执行到那个程序特殊状态时就会抛出异常;异步异常的跟踪则困难的多。所以同步和异步异常暗示同步或异步代码可能抛出异常(synchronous and asynchronous exceptions imply synchronous or asynchronous code in your program that might raise exceptions.)

6.因为文章中涉及到了所谓的事件驱动模型,现在我们来了解一下什么是事件驱动模型,这是十分重要的。

 或许每个软件从业者都有从学习控制台应用程序到学习可视化编程的转变过程,控制台应用程序的优点在于可以方便的练习某个语言的语法和开发习惯(如.net和java),而可视化编程的学习又可以非常方便开发出各类人机对话界面(HMI)。可视化编程或许是一个初学者开始对软件感兴趣的开始,也可能是一个软件学习的里程碑点,因为我们可以使用各类软件集成开发环境(IDE)方便的在现成的界面窗口上拖放各类组件(Component),这类组件包括我们常见的按钮(Button),单选按钮(Radio Button),复选框等(Checkbox)。这样的拖放式开发方式不但方便,而且窗口会立竿见影的显示在我们的面前,这对于一个软件初学者而言或许是一件非常有成就感的事情。

  但是很多软件学习者在学习可视化开发的过程中,只是非常表面的来理解可视化编程,他们可能认为能够使用拖放方式完成一个界面就非常值得称道,但是很少有人会认真的去理解编程语言对于可视化编程组件的支持和整合,在Softworks软件人才培训中心的两年教学过程,我深刻的感受到了这一点,因此下文将会结合我的教学经验来讲解可视化编程过程中最为关键的“事件驱动模型”。

  时间驱动模型中,一般会有三个对象。

Event Source(事件源):时间发生的场所,通常就是各种的组件,例如按钮,窗口,菜单等。

Event(事件):事件封装了界面组件上发生的特定的组件(通常就是一次用户的操作),如果程序需要获取界面组件上所发生的事件的相关信息,一般通过Event对象来获得。

Event Listener(事件监听器):负责监听事件源所发生的场所。并且对各种事件作出相应的相应。

Android的时间处理的机制是一种委托式事件处理方式:普通组件将整个事件委托给特定的对象;当该事件源发生特定的事件的时候,就通知所委托的事件监听器,由事件监听器来处理这事件。

7.如何利用CachedThreadPool来执行异步任务。一般来说,执行异步任务的时候,有俩种方式,一种是实现了Runnable接口,另外一种是实现了Callable接口。但是这俩种接口是有区别的。执行实现了Callable接口任务的时候,可以有返回值的。

 (1)执行实现了Runnable接口的任务:

Executor框架_第4张图片

Executor框架_第5张图片

从结果中可以看出,pool-1-thread-1和pool-1-thread-2均被调用了两次,这是随机的,execute会首先在线程池中选择一个已有空闲线程来执行任务,如果线程池中没有空闲线程,它便会创建一个新的线程来执行任务。如果是有空闲的线程的话,那么就会使用空闲的线程来执行任务,这是要注意的。


(2)执行的是实现了Callable接口的任务。

在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable task) 方法来执行,并且返回一个 Future,是表示任务等待完成的 Future。泛型表示的是返回的结果是什么类型的。这是要注意的。


    Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。


    当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。

package Concurrency;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
//最主要是要认识到Main方法中也是一个Main线程。并且是可以执行的。
//这里其实也可以看作是有俩个线程在执行。一个是Main方法中的线程,另外一个是在线程池中执行的所有的线程。
//所以在Main方法中,当Main方法获得了执行的权利以后,那么就会执行另外一个for循环中的内容。可是如果这个任务还没有
//执行完毕,那么就会等待它执行完毕。这是要注意的。


public class CallableDemo {
    public static void main(String[] ags) {
        ExecutorService executorService= Executors.newCachedThreadPool();
        List> resultList = new ArrayList>();
        for (int i = 0; i < 10; i++) {
            Future future = executorService.submit(new TaskWithResul(i));
            resultList.add(future);
        }


        for (Future fs : resultList) {
        try{
                  while (!fs.isDone()) ;//如果这个任务还没有执行完毕,那么就会一直等待。这是要注意的。
                
                    System.out.println(fs.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }finally{
                executorService.shutdown();
                }
            }


        }
        }


    




class TaskWithResul implements Callable {
    private int id;


    public TaskWithResul(int id) {
        this.id=id;
    }
    @Override
    public String call() throws Exception {
        System.out.println("call 方法被自动调用" + Thread.currentThread().getName());
        return "call方法被自动调用,任务返回的结果是:"+id+"  "+Thread.currentThread().getName();
    }
}

Executor框架_第6张图片

    从结果中可以同样可以看出,submit也是首先选择空闲线程来执行任务,如果没有,才会创建新的线程来执行任务。另外,需要注意:如果Future的返回尚未完成,则get()方法会阻塞等待,直到Future完成返回,可以通过调用isDone()方法判断Future是否完成了返回。

8。如何自定义线程。ThreadPoolExecutor是创建线程池的核心的部分。这是要注意的。

Executor框架_第7张图片


从结果中可以看出,七个任务是在线程池的三个线程上执行的。这里简要说明下用到的ThreadPoolExecuror类的构造方法中各个参数的含义。   

public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long         keepAliveTime, TimeUnit unit,BlockingQueue workQueue)

corePoolSize:线程池中所保存的核心线程数,包括空闲线程。

maximumPoolSize:池中允许的最大线程数。

keepAliveTime:线程池中的空闲线程所能持续的最长时间。

unit:持续时间的单位。

workQueue:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。

    根据ThreadPoolExecutor源码前面大段的注释,我们可以看出,当试图通过excute方法讲一个Runnable任务添加到线程池中时,按照如下顺序来处理:

    1、如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;

    2、如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);

    3、如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;

    4、如果线程池中的线程数量等于了maximumPoolSize,有4种才处理方式(该构造方法调用了含有5个参数的构造方法,并将最后一个构造方法为RejectedExecutionHandler类型,它在处理线程溢出时有4种方式,这里不再细说,要了解的,自己可以阅读下源码)。

    总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize。

    另外,当线程池中的线程数量大于corePoolSize时,如果里面有线程的空闲时间超过了keepAliveTime,就将其移除线程池,这样,可以动态地调整线程池中线程的数量。

    我们大致来看下Executors的源码,newCachedThreadPool的不带RejectedExecutionHandler参数(即第五个参数,线程数量超过maximumPoolSize时,指定处理方式)的构造方法如下:

[java]  view plain  copy
 
  1. public static ExecutorService newCachedThreadPool() {  
  2.     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,  
  3.                                   60L, TimeUnit.SECONDS,  
  4.                                   new SynchronousQueue());  
  5. }  
    它将corePoolSize设定为0,而将maximumPoolSize设定为了Integer的最大值,线程空闲超过60秒,将会从线程池中移除。由于核心线程数为0,因此每次添加任务,都会先从线程池中找空闲线程,如果没有就会创建一个线程(SynchronousQueue决定的,后面会说)来执行新的任务,并将该线程加入到线程池中,而最大允许的线程数为 Integer的最大值 ,因此这个线程池理论上可以不断扩大。

    再来看newFixedThreadPool的不带RejectedExecutionHandler参数的构造方法,如下:

[java]  view plain  copy
 
  1. public static ExecutorService newFixedThreadPool(int nThreads) {  
  2.     return new ThreadPoolExecutor(nThreads, nThreads,  
  3.                                   0L, TimeUnit.MILLISECONDS,  
  4.                                   new LinkedBlockingQueue());  
  5. }  
    它将 corePoolSize和maximumPoolSize都设定为了nThreads,这样便实现了线程池的大小的固定,不会动态地扩大,另外,keepAliveTime设定为了0,也就是说线程只要空闲下来,就会被移除线程池,敢于LinkedBlockingQueue下面会说。

    下面说说几种排队的策略:

    1、直接提交。缓冲队列采用 SynchronousQueue,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中。直接提交通常要求无界 maximumPoolSizes(Integer.MAX_VALUE) 以避免拒绝新提交的任务。newCachedThreadPool采用的便是这种策略。

    2、无界队列。使用无界队列(典型的便是采用预定义容量的 LinkedBlockingQueue,理论上是该缓冲队列可以对无限多的任务排队)将导致在所有 corePoolSize 线程都工作的情况下将新任务加入到缓冲队列中。这样,创建的线程就不会超过 corePoolSize,也因此,maximumPoolSize 的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。newFixedThreadPool采用的便是这种策略。

    3、有界队列。当使用有限的 maximumPoolSizes 时,有界队列(一般缓冲队列使用ArrayBlockingQueue,并制定队列的最大长度)有助于防止资源耗尽,但是可能较难调整和控制,队列大小和最大池大小需要相互折衷,需要设定合理的参数。

   补充说明:在这里因为突然想起来了,所以现在又来补充一下:

   现在说说shutdownNow和shutdown的区别:可以关闭 ExecutorService,这将导致其拒绝新任务。提供两个方法来关闭 ExecutorService。shutdown() 方法在终止前允许执行以前提交的任务,而 shutdownNow() 方法阻止等待任务启动并试图停止当前正在执行的任务。在终止时,执行程序没有任务在执行,也没有任务在等待执行,并且无法提交新任务。应该关闭未使用的 ExecutorService 以允许回收其资源。 

下列方法分两个阶段关闭 ExecutorService。第一阶段调用 shutdown 拒绝传入任务,然后调用 shutdownNow(如有必要)取消所有遗留的任务: 

[java]  view plain  copy
  1. void shutdownAndAwaitTermination(ExecutorService pool) {  
  2.   pool.shutdown(); // Disable new tasks from being submitted  
  3.   try {  
  4.     // Wait a while for existing tasks to terminate  
  5.     if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {  
  6.       pool.shutdownNow(); // Cancel currently executing tasks  
  7.       // Wait a while for tasks to respond to being cancelled  
  8.       if (!pool.awaitTermination(60, TimeUnit.SECONDS))  
  9.           System.err.println("Pool did not terminate");  
  10.     }  
  11.   } catch (InterruptedException ie) {  
  12.     // (Re-)Cancel if current thread also interrupted  
  13.     pool.shutdownNow();  
  14.     // Preserve interrupt status  
  15.     Thread.currentThread().interrupt();  
  16.   }  
  17. }  

shutdown调用后,不可以再submit新的task,已经submit的将继续执行。

shutdownNow试图停止当前正执行的task,并返回尚未执行的task的list





你可能感兴趣的:(Java并发编程)