JAVA 并发编程之十:线程模型2:Executor框架 + 线程池ThreadPool 的四种类型

版权声明:本文为博主原创文章,未经允许不得转载,如有问题,欢迎指正,谢谢! https://blog.csdn.net/cbk861110/article/details/89197732

前言

1. Executor框架包括:Executor,Executors,ExecutorService,CompletionService,Future,Callable,FutureTask等。

2. 线程|任务 区别: 实现Runnable的类应该被看作一项任务,而不是一个线程。在Java多线程中我们一定要有一个明确的理解,任务和线程是不同的概念。可以使用线程(Thread)执行任务(比如Runnable),但任务不是线程。

2. Runnable|Callable 区别: Java多线程中有两种不同类型的任务,Runnable类型任务(无返回值)与Callable类型任务(有返回值)。

1. 引子

初学Java多线程,常使用ThreadRunnable创建、启动线程。如下例:

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
});
t1.start();

我们需要自己创建、启动Thread对象。

 

2. 使用Executor执行线程

一些已有的执行器可以帮我们管理Thread对象。你无需自己创建与控制Thread对象。比如,你不用在代码中编写new Thread或者thread1.start()也一样可以使用多线程。如下例:

ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {//5个任务
    exec.submit(new Runnable() {
        @Override
        public void run() {            
            System.out.println(Thread.currentThread().getName()+" doing task");
         }
     });
}
exec.shutdown();  //关闭线程池

输出如下:

pool-1-thread-2 doing task
pool-1-thread-1 doing task
pool-1-thread-3 doing task
pool-1-thread-4 doing task
pool-1-thread-5 doing task

从输出我们可以看到,exec使用了线程池1中的5个线程做了这几个任务。

扫描二维码关注公众号,回复: 5843009 查看本文章

这个例子中exec这个Executor负责管理任务,所谓的任务在这里就是实现了Runnable接口的匿名内部类。至于要使用几个线程,什么时候启动这些线程,是用线程池还是用单个线程来完成这些任务,我们无需操心。完全由exec这个执行器来负责。在这里exec(newCachedThreadPool)指向是一个可以根据需求创建新线程的线程池。

Executors

相当于执行器的工厂类,包含各种常用执行器的工厂方法,可以直接创建常用的执行器。几种常用的执行器如下:

Executors.newCachedThreadPool,根据需要可以创建新线程的线程池。线程池中曾经创建的线程,在完成某个任务后也许会被用来完成另外一项任务。

Executors.newFixedThreadPool(int nThreads) ,创建一个可重用固定线程数的线程池。这个线程池里最多包含nThread个线程。

Executors.newSingleThreadExecutor() ,创建一个使用单个 worker 线程的 Executor。即使任务再多,也只用1个线程完成任务。

Executors.newSingleThreadScheduledExecutor() ,创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期执行

newSingleThreadExecutor例子如下:

ExecutorService exec = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
    exec.execute(new Runnable() {//execute方法接收Runnable对象,无返回值
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    });
}
exec.shutdown();

输出如下:

pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

可以看出,虽然有5个任务(5个new Runnable),但是只由1个线程来完成。

最佳实践:我们应该使用现有Executor或ExecutorService实现类。比如前面说的newCachedThreadPool可以使用线程池帮我们降低开销(创建一个新的线程是有一定代价的),而newFixedThreadPool则可以限制并发线程数。即,我们一般使用Executors的工厂方法来创建我们需要的执行器。

Executor与ExecutorService的常用方法

execute方法:

Executor接口只有void execute(Runnable command)方法。从方法声明中我们可以看到入参为Runnable类型对象。常用的例子如下:

Executor executor = anExecutor;
executor.execute(new RunnableTask1());

但里面具体怎么执行,是否调用线程执行由相应的Executor接口实现类决定。比如前面的newCachedThreadPool使用线程池来进行执行。Executor将任务提交与每个任务如何运行(如何使用线程、调度)相分离。

submit方法:

ExecutorService接口继承自Executor接口,扩展了父接口中的execute方法。有两个常用的submit方法

Future<?> submit(Runnable task) 
<T> Future<T> submit(Callable<T> task)

可以看到这两个常用方法一个接收Runnable类型入参,一个接收Callable类型入参。Callable入参允许任务返回值,而Runnable无返回值。也就是说如果我们希望线程有一个返回结果,我们应该使用Callable类型入参。

invokeAll与invokeAny方法:

批量执行一组Callable任务。其中invokeAll是等所有任务完成后返回代表结果的Future列表。而invokeAny是等这一批任务中的任何一个任务完成后就返回。从两个方法的返回结果我们也可以看出两个方法的不同:

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
<T> T invokeAny(Collection<? extends Callable<T>> tasks)

invokeAll返回的是List<Future>,而invoke返回的是T

shutdown()方法:

启动一次顺序关闭,执行以前提交的任务,但不接受新任务。执行此方法后,线程池等待任务结束后就关闭,同时不再接收新的任务。如果执行完shutdown()方法后,再去执行execute方法则直接抛出RejectedExecutionException。不要问我为什么知道...刚从坑里爬出来。

原则:只要ExecutorService(线程池)不再使用,就应该关闭,以回收资源。要注意这个不再使用

上述方法较多,可以配合后面的实例进行理解。可以先记住execute方法与shutdown方法。

3. 使用Callable与Future

Callable接口

Runnable接口中的public void run()方法无返回值,如果我们希望线程运算后将结果返回,使用Runnable就无能为力。这时候我们应使用CallableCallable代表有返回值的任务。一个实现Callable接口的类如下所示:

class CalcTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName();
    }
}

这个任务比较简单,就是返回当前线程的名字。与Runnable相比较有一个返回值,在这里返回值类型为String,也可以为其他类型。

使用如下代码进行调用:

ExecutorService exec = Executors.newCachedThreadPool();
List<Callable<String>> taskList = new ArrayList<Callable<String>>();
/* 往任务列表中添加5个任务 */
for (int i = 0; i < 5; i++) {
    taskList.add(new CalcTask());
}
/* 结果列表:存放任务完成返回的值 */
List<Future<String>> resultList = new ArrayList<Future<String>>();
try {
    /*invokeAll批量运行所有任务, submit提交单个任务*/
    resultList = exec.invokeAll(taskList);
} catch (InterruptedException e) {
    e.printStackTrace();
}
try {
    /*从future中输出每个任务的返回值*/
    for (Future<String> future : resultList) {
        System.out.println(future.get());//get方法会阻塞直到结果返回
    }
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

输出如下:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5

 

Future接口

概念:Task Submitter把任务提交给Executor执行,他们之间需要一种通 讯手段,这种手段的具体实现,通常叫做Future。Future通常包括 get(阻塞至任务完成), cancel,get(timeout)(等待一段时间) 等等。Future也用于异步变同步的场景。

上面的例子中我们使用了Future接口。Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。上面的例子中exec执行器执行了一个Callable类型的任务列表然后得到了Futuer类型的结果列表resultList。

get方法

等待计算完成,然后获取其结果。

isDone方法

用来查询任务是否做完,例子如下:

/*新建一个Callable任务*/
Callable<Integer> callableTask = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        System.out.println("Calculating 1+1!");
        TimeUnit.SECONDS.sleep(2);//休眠2秒
        return 2;
    }
}; 
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> result = executor.submit(callableTask);
executor.shutdown();
while(!result.isDone()){//isDone()方法可以查询子线程是否做完
    System.out.println("子线程正在执行");
    TimeUnit.SECONDS.sleep(1);//休眠1秒
}
try {
    System.out.println("子线程执行结果:"+result.get());
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

输出如下:

Calculating 1+1!
子线程正在执行
子线程正在执行
子线程执行结果:2

4.FutureTask

FutureTask类是 Future 接口的一个实现。FutureTask类实现了RunnableFuture接口,RunnableFuture继承了Runnable接口和Future接口,所以:

  1. FutureTask可以作为Runnable被线程执行
  2. 可以作为Future得到传入的Callable对象的返回值
    例子如下:
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
   @Override
   public Integer call() throws Exception {
        System.out.println("futureTask is wokring 1+1!");
        return 2;
   }
});
Thread t1 = new Thread(futureTask);//1.可以作为Runnable类型对象使用
t1.start();
try {
   System.out.println(futureTask.get());//2.可以作为Future类型对象得到线程运算返回值
} catch (ExecutionException e) {
   e.printStackTrace();
}

输出如下:

futureTask is wokring 1+1!
2

可以看出FutureTask可以当作一个有返回值的Runnable任务来用。

分析:FutureTask<Integer> futureTask = new FutureTask<>(new Callable...)相当于把Callable任务转换为Runnable任务,就可以使用线程来执行该任务。而futureTask.get()相当于将Callable转化为Future,从而得到异步运算的结果。

ExecutorService执行器除了接收Runnable与Callable类型的入参,也可以接收FutureTask类型,例子如下:

FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        System.out.println("futureTask is wokring 1+1!");
        TimeUnit.SECONDS.sleep(2);
        return 2;
    }
});
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(futureTask);//也可以使用execute,证明其是一个Runnable类型对象
executor.shutdown();
while(!futureTask.isDone()){
    System.out.println("子线程还没做完,我再睡会");
    TimeUnit.SECONDS.sleep(1);
}
try {
    System.out.println("子线程运行的结果:"+futureTask.get());
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

 

5. 线程池

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

    public static ExecutorService newFixedThreadPool(int nThreads)

    创建固定数目线程的线程池。

    public static ExecutorService newCachedThreadPool()

    创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线   程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

    public static ExecutorService newSingleThreadExecutor()

    创建一个单线程化的Executor。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

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

    这四种方法都是用的Executors中的ThreadFactory建立的线程,下面就以上四个方法做个比较


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)


    一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。(该段话摘自《Thinking in Java》第四版)

资料整理来自:
https://www.cnblogs.com/zhrb/p/6372799.html
http://www.cnblogs.com/jobs/archive/2010/07/29/1788156.html
https://blog.csdn.net/ns_code/article/details/17465497 

猜你喜欢

转载自blog.csdn.net/cbk861110/article/details/89197732