并发编程:Executor、Executors、ExecutorService

在Java 5之后引入Executor框架使用了新的启动、调度和管理线程的API , 其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象用Executor在构造器中。Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务的线程相当于消费者,并用Runnable来表示任务,Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能监视等机制。

一、Executor的UML图:(常用的几个接口和子类)

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

二、Executor和ExecutorService

Executor:一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类,一般来说,Runnable任务开辟在新线程中的使用方法为:new Thread(new RunnableTask())).start(),但在Executor中,可以使用Executor而不用显示地创建线程:executor.execute(new RunnableTask()); // 异步执行

ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,返回 Future 对象,以及可跟踪一个或多个异步任务执行状况返回Future的方法;可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

通过 ExecutorService.submit() 方法返回的 Future 对象,可以调用isDone()方法查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法来获取该结果。你也可以不用isDone()进行检查就直接调用get()获取结果,在这种情况下,get()将阻塞,直至结果准备就绪,还可以取消任务的执行。Future 提供了 cancel() 方法用来取消执行 pending 中的任务。ExecutorService 部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
public interface ExecutorService extends Executor {

       void shutdown();

       <T> Future<T> submit(Callable<T> task);

       <T> Future<T> submit(Runnable task, T result);

       <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

}

三、Executors类: 主要用于提供线程池相关的操作

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

例如:public static ExecutorService newFiexedThreadPool(int Threads) 创建固定数目线程的线程池。

其它线程池见另一篇文章:

https://blog.csdn.net/weixin_38898423/article/details/106569456

四、Executor VS ExecutorService VS Executors

正如上面所说,这三者均是 Executor 框架中的一部分。Java 开发者很有必要学习和理解他们,以便更高效的使用 Java 提供的不同类型的线程池。总结一下这三者间的区别,以便大家更好的理解:

Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口

Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的对象。

Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。

Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。可以通过 《Java Concurrency in Practice》 一书了解更多关于关闭线程池和如何处理 pending 的任务的知识。

Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。

下面给出一个Executor执行Callable任务的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class CallableDemo{  

    public static void main(String[] args){  

        ExecutorService executorService = Executors.newCachedThreadPool();  

        List<Future<String>> resultList = new ArrayList<Future<String>>();  

        //创建10个任务并执行  

        for (int i = 0; i < 10; i++){  

            //使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中  

            Future<String> future = executorService.submit(new TaskWithResult(i));  

            //将任务执行结果存储到List中  

            resultList.add(future);  

        }  

        //遍历任务的结果  

        for (Future<String> fs : resultList){  

                try{  

                    while(!fs.isDone);//Future返回如果没有完成,则一直循环等待,直到Future返回完成

                    System.out.println(fs.get());     //打印各个线程(任务)执行的结果  

                }catch(InterruptedException e){  

                    e.printStackTrace();  

                }catch(ExecutionException e){  

                    e.printStackTrace();  

                }finally{  

                    //启动一次顺序关闭,执行以前提交的任务,但不接受新任务

                    executorService.shutdown();  

                }  

        }  

    }  

}  



class TaskWithResult implements Callable<String>{  

    private int id;  

    public TaskWithResult(int id){  

        this.id = id;  

    }  


    /**

     * 任务的具体过程,一旦任务传给ExecutorService的submit方法,

     * 则该方法自动在一个线程上执行

     */  

    public String call() throws Exception {

        System.out.println("call()方法被自动调用!!!    " + Thread.currentThread().getName());  

        //该返回结果将被Future的get方法得到

        return "call()方法被自动调用,任务返回的结果是:" + id + "    " + Thread.currentThread().getName();  

    }  

}

五、比较Executor和new Thread()

new Thread的弊端如下:

a. 每次new Thread新建对象性能差。

b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。

c. 缺乏更多功能,如定时执行、定期执行、线程中断。

相比new Thread,Java提供的四种线程池的好处在于:

a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。

b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。

c. 提供定时执行、定期执行、单线程、并发数控制等功能。