线程池

主要用于概念扫盲和梳理。
本文主要为什么使用线程池,ThreadPoolExecutor构造函数,常见的线程池种类。

一. 为什么用线程池

1. 系统开销

创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率

例如:
记创建线程消耗时间T1,执行任务消耗时间T2,销毁线程消耗时间T3
如果T1+T3>T2,那么是不是说开启一个线程来执行这个任务太不划算了!

线程池缓存线程,可复用的闲置线程来执行新任务,避免了T1+T3带来的系统开销

2. 系统资源

线程并发数量过多,抢占系统资源从而导致阻塞

我们知道线程能共享系统资源,如果同时执行的线程过多,就有可能导致系统资源不足而产生阻塞的情况

运用线程池能有效的控制线程最大并发数,避免以上的问题

3. 管理线程

对线程进行一些简单的管理

比如:延时执行、定时循环执行的策略

运用线程池都能进行很好的实现>

二. 继承关系

总览

线程池

常用的几个类或接口

ExecutorService

线程池接口,继承于Executor接口,多了Submit()shutdown()等方法

ScheduledExecutorService

功能和Timer类似,解决那些需要任务重复执行的问题,负责线程的调度

ThreadPoolExecutor

线程池的实现类

ScheduledThreadPoolExecutor

继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

Executors

一个工具类,提供几个创建线程池的方法

Future 接口

它和FutureTask类(实现了Future接口)都用来表示异步计算的结果。

三. 构造线程池

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。

  • 在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类
  • 对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置

构造函数

七个参数 :
心 总 时 纲 队 厂 异

五个参数的构造函数

1
2
3
4
5
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)

六个参数的构造函数-1

1
2
3
4
5
6
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)

六个参数的构造函数-2

1
2
3
4
5
6
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)

七个参数的构造函数

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

参数解释

1. int corePoolSize —核心线程数最大值

  • 线程池新建线程的时候
    1. 如果当前线程总数小于corePoolSize,则新建的是核心线程
    2. 如果超过corePoolSize,则新建的是非核心线程
  • 核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
  • 如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉

2. int maximumPoolSize—线程总数最大值

  • 线程总数 = 核心线程数 + 非核心线程数

3. long keepAliveTime—非核心线程闲置超时时长

  • 一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
  • 如果设置allowCoreThreadTimeOut = true,则会作用于核心线程

4. TimeUnit unit—量纲

keepAliveTime的单位,TimeUnit是一个枚举类型,其包括:

单位 时长
NANOSECONDS 1微毫秒 = 1微秒 / 1000
MICROSECONDS 1微秒 = 1毫秒 / 1000
MILLISECONDS 1毫秒 = 1秒 /1000
SECONDS
MINUTES
HOURS 小时
DAYS

5. BlockingQueue workQueue–任务队列

  • 维护着等待执行的Runnable对象
  • 当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

常用的workQueue类型:

SynchronousQueue

它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现线程数达到了maximumPoolSize而不能新建线程的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大

LinkedBlockingQueue

基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE

这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。

由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

ArrayBlockingQueue

基于数组的先进先出队列,此队列创建时必须指定大小

可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

DelayQueue

队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

6. ThreadFactory threadFactory

主要用来创建线程,是一个接口,new他的时候需要实现他的Thread newThread(Runnable r)方法,一般用不上

AsyncTask新建线程池的threadFactory参数源码:

1
2
3
4
5
6
7
new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);

public Thread new Thread(Runnable r) {
return new Thread(r,"AsyncTask #" + mCount.getAndIncrement());
}
}

7. RejectedExecutionHandler handler

表示当拒绝处理任务时的策略,有以下四种取值:

取值 策略
ThreadPoolExecutor.AbortPolicy(默认) 丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy 也是丢弃任务,但是不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy 由调用线程处理该任务,线程调用运行该任务的execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度

ThreadPoolExecutor的策略

上面介绍参数的时候其实已经说到了ThreadPoolExecutor执行的策略,这里给总结一下,当一个任务被添加进线程池时:

  1. 核心线程满了吗
    1.1 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
    1.2 线程数量达到了corePools,则将任务移入队列等待
  2. 队列满了吗
    2.1 队列已满,新建线程(非核心线程)执行任务
    2.2 队列已满,总线程数又达到了maximumPoolSize,就会由RejectedExecutionHandler抛出异常

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

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

ThreadPoolExecutor类中重要方法

方法 作用
execute() 通过这个方法可以向线程池提交一个任务,交由线程池去执行
submit() 实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果
shutdown() 不会立即终止线程池,而是要所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow() 立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

四. Executor与Executors

关系

Java里面线程池的顶级接口是 Executor,不过真正的线程池接口是 ExecutorService, ExecutorService 的默认实现是 ThreadPoolExecutor;普通类 Executors 里面调用的就是 ThreadPoolExecutor。

对比

  • Executor 框架
    Executor框架在Java 5中被引入,Executor 框架是一个根据一组执行策略调用、调度、执行和控制的异步任务的框架
    无限制的创建线程会引起应用程序内存溢出,所以创建一个线程池是个更好的的解决方案,因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executor 框架可以非常方便的创建一个线程池。

  • Executors 类
    Executors为Executor、ExecutorService、ScheduledExecutorService、ThreadFactory 和 Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池。

五. 常见四种线程池

Java通过Executors提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的

1. CachedThreadPool()–可缓存线程池

特点

  1. 线程数无限制
  2. 有空闲线程则复用空闲线程,若无空闲线程则新建线程
  3. 一定程度上减少频繁创建/销毁线程,减少系统开销

场合

缓存型池子通常用于执行一些生存期很短的异步型任务 因此在一些面向连接的 daemon 型 SERVER 中用得不多。
但对于生存期短的异步任务,它是 Executor 的首选。

源码:

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

2. FixedThreadPool(int nThreads)–定长线程池

特点

  1. 可控制线程最大并发数(同时执行的线程数)
  2. 超出的线程会在队列中等待

场合

FixedThreadPool 多数针对一些很稳定很固定的正规并发线程,多用于服务器。

//threadFactory => 创建线程的方法,这就是我叫你别理他的那个星期六!你还看!
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);
源码:

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

2个参数的构造方法源码,不用我贴你也知道他把星期六放在了哪个位置!所以我就不贴了,省下篇幅给我扯皮

3. ScheduledThreadPool(int corePoolSize)–定核心线程池

  1. 支持定时及周期性任务执行。

创建方法:

//nThreads => 最大线程数即maximumPoolSize
ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);
源码:

1
2
3
4
5
6
7
8
9
10
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}

4. SingleThreadExecutor()–单线程化的线程池

  1. 有且仅有一个工作线程执行任务
  2. 所有任务按照指定顺序执行,即遵循队列的入队出队规则

创建方法:

ExecutorService singleThreadPool = Executors.newSingleThreadPool();

源码:

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

六. 实际例子

Executor 执行 Runnable 任务

通过 Executors 的以上四个静态工厂方法获得 ExecutorService 实例,而后调用该实例的 execute(Runnable command)方法即可。一旦 Runnable 任务传递到 execute()方法,该方法便会自动在一个线程上执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.ExecutorService;   
import java.util.concurrent.Executors;

public class TestCachedThreadPool{
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
// ExecutorService executorService = Executors.newFixedThreadPool(5);
// ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++){
executorService.execute(new TestRunnable());
System.out.println("************* a" + i + " *************");
}
executorService.shutdown();
}
}

class TestRunnable implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName() + "线程被调用了。");
}
}

输出:(每次可能不一样)

1
2
3
4
5
6
7
8
9
10
************* a0 *************
pool-1-thread-1线程被调用了。
************* a1 *************
pool-1-thread-2线程被调用了。
************* a2 *************
pool-1-thread-3线程被调用了。
************* a3 *************
************* a4 *************
pool-1-thread-1线程被调用了。
pool-1-thread-3线程被调用了。

Executor 执行 Callable 任务

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

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
import java.util.ArrayList;   
import java.util.List;
import java.util.concurrent.*;

public class CallableDemo{
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> resultList = new ArrayList<Future<String>>();
for (int i = 0; i < 10; i++){ //创建10个任务并执行
//使用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();
}
}

输出:(每次可能不一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
call()方法被自动调用!!!    pool-1-thread-1
call()方法被自动调用!!! pool-1-thread-3
call()方法被自动调用!!! pool-1-thread-1
call()方法被自动调用!!! pool-1-thread-2
call()方法被自动调用!!! pool-1-thread-8
call()方法被自动调用!!! pool-1-thread-5
call()方法被自动调用!!! pool-1-thread-9
call()方法被自动调用!!! pool-1-thread-4
call()方法被自动调用!!! pool-1-thread-6
call()方法被自动调用!!! pool-1-thread-7
call()方法被自动调用,任务返回的结果是:0 pool-1-thread-1
call()方法被自动调用,任务返回的结果是:1 pool-1-thread-2
call()方法被自动调用,任务返回的结果是:2 pool-1-thread-3
call()方法被自动调用,任务返回的结果是:3 pool-1-thread-4
call()方法被自动调用,任务返回的结果是:4 pool-1-thread-5
call()方法被自动调用,任务返回的结果是:5 pool-1-thread-6
call()方法被自动调用,任务返回的结果是:6 pool-1-thread-7
call()方法被自动调用,任务返回的结果是:7 pool-1-thread-8
call()方法被自动调用,任务返回的结果是:8 pool-1-thread-1
call()方法被自动调用,任务返回的结果是:9 pool-1-thread-9

参考文章
线程池,这一篇或许就够了
Java并发编程:线程池的使用
官方文档
【Java多线程】线程池的工作原理详解(上)
极客学院

> >