java线程的创建非常昂贵,需要JVM和OS(操作系统)互相配合完成大量的工作。而java高并发频繁的创建和销毁线程的操作是非常低效的,如何降低java线程的创建成本,就必须要使用到线程池。
1 线程池的类继承体系
1.1 Executor
它是 Java 异步目标任务的“执行者”接口,其目标是来执行目标任务。“执行者”Executor提供了 execute()接口来执行已提交的 Runnable 执行目标实例。Executor 作为执行者的角色,存在的目的是“任务提交者”与“任务执行者”分离开来的机制。
1.2 ExecutorService
ExecutorService 继承于 Executor。。它是 Java 异步目标任务的“执行者服务“接口,它对外提供异步任务的接收服务,ExecutorService 提供了“接收异步任务、并转交给执行者”的方法,如submit 系列方法、invoke 系列方法等等。
1.3 AbstractExecutorService
AbstractExecutorService 是一个抽象类 ,它 实 现 了 ExecutorService 接口。AbstractExecutorService 存在的目的是为 ExecutorService 中的接口提供了默认实现。
1.4 ThreadPoolExecutor
ThreadPoolExecutor就是“线程池”实现类,它继承于 AbstractExecutorService 抽象类。是 JUC 线程池的核心实现类。
1.5 ScheduledExecutorService
ScheduledExecutorService 是一个接口,它继承于于 ExecutorService。它是一个可以完成“延 时”“周期性”任务的调度线程池接口,其功能和 Timer/TimerTask 类似。
1.6 ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor 继承于 ThreadPoolExecutor,它提供了 ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。ScheduledThreadPoolExecutor 类似于 Timer , 但 是 在 高 并 发 程 序 中ScheduledThreadPoolExecutor 的性能要优于 Timer。
1.7 Executors
Executors 是个静态工厂类,它通 过 静 态 工 厂 方 法 返 回 ExecutorService 、ScheduledExecutorService 等线程池示例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。
2 Executors 四种快捷创建线程池方法
2.1 newSingleThreadExecutor 创建“单线程化线程池”
也就是只有一条线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池,能保证所有任务按照指定顺序(如FIFO)执行
public class TestSingleThreadExecutor {
public static final int SLEEP_GAP = 500;
/**
* 异步任务的执行目标类
*/
static class TargetTask implements Runnable {
static AtomicInteger taskNo = new AtomicInteger(1);
private String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
@Override
public void run() {
Print.tco("任务:" + taskName + " doing");
// 线程睡眠一会
try {
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
e.printStackTrace();
}
Print.tco(taskName + " 运行结束.");
}
}
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
运行代码可以得出:
1.单线程化的线程池中的任务,是按照提交的次序,顺序执行的
2.池中的唯一线程的存活时间是无限的
3.当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
总体来说,单线程化的线程池所适用的场景是:任务按照提交次序,一个任务一个任务逐个执行的场景。
2.2 newFixedThreadPool 创建“固定数量的线程池”
public class TestNewFixedThreadPool {
public static final int SLEEP_GAP = 500;
/**
* 异步任务的执行目标类
*/
static class TargetTask implements Runnable {
static AtomicInteger taskNo = new AtomicInteger(1);
private String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
@Override
public void run() {
Print.tco("任务:" + taskName + " doing");
// 线程睡眠一会
try {
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
e.printStackTrace();
}
Print.tco(taskName + " 运行结束.");
}
}
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
运行结果可得知:
1.如果线程数量没有达到“固定数量”,则每次提交一个任务池内就创建一个新的线程,直到到达固定的数量
2.线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3.如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)。
适用场景:需要任务长期执行的场景。“固定数量的线程池”的线程数能够比较稳定保证一个数,能够避免频繁回收线程和创建线程,故适用于处理 CPU 密集型的任务,在 CPU 被工作线程长时间使用的情况下,能确保尽可能少的分配线程。
弊端:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无线增大,使服务器资源迅速耗尽。
2.3 newCachedThreadPool 创建“可缓存线程池”
public class TestNewCacheThreadPool {
public static final int SLEEP_GAP = 500;
/**
* 异步任务的执行目标类
*/
static class TargetTask implements Runnable {
static AtomicInteger taskNo = new AtomicInteger(1);
private String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
@Override
public void run() {
Print.tco("任务:" + taskName + " doing");
// 线程睡眠一会
try {
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
e.printStackTrace();
}
Print.tco(taskName + " 运行结束.");
}
}
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
pool.shutdown();
}
}
“可缓存线程池”的特点,大致如下:
1.在接收新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池会添加新线程来处理任务。
2.此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
3.如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,那么就会回收空闲(60 秒不执行任务)线程。
适用场景:需要快速处理突发性强、耗时较短的任务场景,如 Netty 的NIO 处理场景、REST API 接口的瞬时削峰场景。“可缓存线程池”的线程数量不固定,只要有空闲线程就会被回收;接收到的新异步任务执行目标,查看是否有线程处于空闲状态,如果没有就直接创建新的线程。
弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能导致创线程过多会而导致资源耗尽。
2.4 newScheduledThreadPool 创建“可调度线程池”
创建一个可调度线程池,池内仅含有一条线程:
创建一个可调度线程池,池内含有 N 条线程,N 的值为输入参数 corePoolSize:
总结
创建的线程池的好处?
1.降低资源消耗.通过重复利用自己创建的线程降低线程创建和销毁造成的消耗.
2.提高响应速度.当任务到达时,任务可以不需要等到线程和粗昂就爱你就能立即执行.
3.提高线程的可管理性.线程是稀缺资源,如果无限的创线程,不仅会消耗资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控
以上是通过 JUC 的 Executors 四个主要的快捷创建线程池方法。不同类型的线程池,其实都是由前面的几个关键配置参数配置而成的。在《阿里巴巴Java开发手册》中,明确禁止使用Executors创建线程池,并要求开发者直接使用ThreadPoolExector或ScheduledThreadPoolExecutor进行创建。这样做是为了强制开发者明确线程池的运行策略,使其对线程池的每个配置参数皆做到心中有数,以规避因使用不当而造成资源耗尽的风险。