【Java 线程池】【一】线程池介绍和基本使用
1 前言
这节开始我们主要是攻克一下Java中的线程池,来深入的分析一下线程池是怎么设计的,线程池的原理是什么等,只有更好的理解原理,才能很好的使用并且规避掉一些问题,那么本节我们先简单介绍下线程池是什么以及平时大家的使用方法。
2 认识线程池
首先大家要知道为什么要有线程池?
Java线程的创建非常昂贵,需要JVM和OS(操作系统)配合完成大量的工作: 1)必须为线程堆栈分配和初始化大量内存块,其中包含至少1MB的栈内存。 2)需要进行系统调用,以便在OS(操作系统)中创建和注册本地线程。
Java高并发应用频繁创建和销毁线程的操作将是非常低效的,而且是不被编程规范所允许的。 如何降低Java线程的创建成本?必须使用到线程池。线程池主要解决了以下两个问题:
1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不 需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任 务,最大限度地对已经创建的线程进行复用,使得性能提升明显。
2)线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲 时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。
在多线程编程中,任务都是一些抽象且离散的工作单元,而线程是使任务异步执行的基本机 制。随着应用的扩张,线程和任务管理也变得非常复杂,为了简化这些复杂的线程管理模式,我们 需要一个“管理者”来统一管理线程及任务分配,这就是线程池。
在JUC中有关线程池的类与接口的架构图大致如图所示。
(1)Executor
Executor是Java异步目标任务的“执行者”接口,其目标是来执行目标任务。“执行者”Executor 提供了execute()接口来执行已提交的Runnable执行目标实例。Executor作为执行者的角色,其目的 是“任务提交者”与“任务执行者”分离开来的机制。它只包含一个函数式方法:
void execute(Runnable command)
(2)ExecutorService
ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务”接口,对外提供异 步任务的接收服务,ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系 列方法、invoke系列方法等。具体如下:
//向线程池提交单个异步任务 <T> Future<T> submit(Callable<T> task); //向线程池提交批量异步任务 <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
(3)AbstractExecutorService
AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService 存在的目的是为ExecutorService中的接口提供默认实现。
(4)ThreadPoolExecutor
ThreadPoolExecutor就是大名鼎鼎的“线程池”实现类,它继承于AbstractExecutorService抽 象类。
ThreadPoolExecutor是JUC线程池的核心实现类。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了 一些基础的数据统计,方便线程的管理和监控。
(5)ScheduledExecutorService
ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时” 和“周期性”任务的调度线程池接口,其功能和Timer/TimerTask类似。
(6)ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线 程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。
ScheduledThreadPoolExecutor类似于Timer,但是在高并发程序中,ScheduledThreadPoolExecutor 的性能要优于Timer。
(7)Executors
Executors 是个静态工厂类 , 它通过静态工厂方法返回 ExecutorService 、 ScheduledExecutorService等线程池实例对象,这些静态工厂方法可以理解为一些快捷的创建线程池的方法。
newSingleThreadExecutor() 创建只有一个线程的线程池
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newCachedThreadPool() 创建一个不限制线程数量的线程池,任何提交的任务都将立即执行, 但是空闲线程会得到及时回收
newScheduledThreadPool() 创建一个可定期或者延时执行任务的线程池
3 线程池的使用
大都是为了提升效率,并发的做某些事情;或者是将一个任务拆分成多个小任务,每个小任务都交给线程池去完成,之后将每个任务的结果合并。
或者是一些异步的场景,将任务直接交给线程池去做...等等,使用的场景非常多。先举个例子,我们是怎么使用线程池的:
public class ThreadPoolTest { public static ExecutorService createThreadPool() { // 创建一个线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor( // 核心线程数设置为2 2, // 最大线程数设置为5 5, // 当线程池中的线程数量大于2,这多出的线程在没有任务执行的最大空闲时间,60 60, // 空闲时间的单位,这里设置秒 TimeUnit.SECONDS, // 线程池的阻塞队列使用LinkedBlockingQueue无界阻塞队列 new LinkedBlockingQueue(), // 创建线程的线程工厂,这里test-pool是设置这个线程池的名字 new DefaultThreadFactory("test-pool"), // 当任务太多,线程池线程不足、阻塞队列满时候采取什么拒绝策略 new ThreadPoolExecutor.CallerRunsPolicy() ); // 返回创建的线程池 return executor; } static class DefaultThreadFactory implements ThreadFactory { private String name; private final AtomicInteger nextId = new AtomicInteger(); DefaultThreadFactory(String name) { this.name = name; } @Override public Thread newThread(Runnable r) { return new Thread(null, r, name + "-" + nextId.incrementAndGet()); } } public static void main(String[] args) { // 创建一个线程池 ExecutorService executor = createThreadPool(); for (int i = 0; i < 1000; i++) { final int num = i; // 封装任务,每个任务就是打印自己是当前第几个号 Runnable task = () -> { System.out.println(num); }; // 往线程池提交任务 executor.execute(task); } } }
上面就是一个线程池使用的小例子:
(1)createThreadPool方法创建出来一个线程池。
(2)然后有1000个任务要跑,每个任务要做的事就是打印自己当前是第几号
(3)封装1000个任务,调用execute方法提交1000个任务
上面的例子是ThreadPoolExecutor线程池,这种线程池是提交任务之后,如果有线程资源空闲会立即执行你提交的任务。
但是其实Java提供的还有另外一种线程池,支持任务的延迟调度、或者任务的周期性的调度的线程池,叫做ScheduledExecutorService,它的使用例子如下:
public class ScheduledThreadPoolTest { // 创建一个调度线程池 public static ScheduledExecutorService createThreadPool() { ScheduledExecutorService executor = new ScheduledThreadPoolExecutor( // 线程池的核心线程数,配置为3 3, // 创建线程的工厂,test-schedule-pool为线程池的名称 new DefaultThreadFactory("test-schedule-pool"), // 任务拒绝策略,线程资源不足的时候,策略是“使用调用线程池的线程来执行” new ThreadPoolExecutor.CallerRunsPolicy() ); return executor; } static class DefaultThreadFactory implements ThreadFactory { private String name; private final AtomicInteger nextId = new AtomicInteger(); DefaultThreadFactory(String name) { this.name = name; } @Override public Thread newThread(Runnable r) { return new Thread(null, r, name + "-" + nextId.incrementAndGet()); } } public static void main(String[] args) { // 获取调度线程池 ScheduledExecutorService executor = createThreadPool(); // 打印当前时间 System.out.println("当前时间:" + new Date()); // 立即执行的任务 Runnable nowTask = () -> { System.out.println("当前时间:" + new Date() + "------执行【立即】任务"); }; // 延迟执行的任务 Runnable delayTask = () -> { System.out.println("当前时间:" + new Date() + "------执行【延迟】任务"); }; // 周期性执行的任务 Runnable periodTask = () -> { System.out.println("当前时间:" + new Date() + "------执行【周期性】任务"); }; // 提交立即任务,有线程空闲立即执行 executor.execute(nowTask); // 提交一次性延迟任务,延迟2秒执行 executor.schedule(delayTask, 2, TimeUnit.SECONDS); // 提交周期性的延迟任务,10秒后每3秒执行一次 executor.scheduleWithFixedDelay(periodTask, 10, 3, TimeUnit.SECONDS); } }
如上图所示,任务的提交时间都是在07:33:39这个时间
(1)其中【立即】任务调用execute方法的任务立即执行了,事实上ScheduleExecutorService继承了ExecutorService接口,具有和他一样的功能,也就是说具有提交任务,线程空闲时立即执行的功能
(2)同时看【延迟】任务,在2秒之后执行了一次,说明具有延迟任务的功能
(3)【周期性】任务,每隔3秒执行一次,说明具有定时任务的功能,是一个定时调度器
上面就是Java提供的两种类型的线程池了,一种是ExecutorService类型的线程池,代表性的子类是ThreadPoolExecutor;
另外一种是ScheduledExecutorService调度类型的线程池,代表性的子类是ScheduledThreadPoolExecutor。
同时JDK为了我们平时线程池的使用方便,提供了一个工具类Executors,里面封装了这两个类型的线程池的创建方法,如下:
public class ExecutorsTest { public static void main(String[] args) { // 创建一个缓存型的线程池,这种线程池每来一个任务就会创建一个线程来执行 Executors.newCachedThreadPool(); // 创建一个固定线程数量的线程池 Executors.newFixedThreadPool(3); // 创建一个单线程的线程池 Executors.newSingleThreadExecutor(); // 创建一个单线程的调度线程池 Executors.newSingleThreadScheduledExecutor(); // 创建一个固定线程数量的调度线程池 Executors.newScheduledThreadPool(3); } }
如上代码所示:Executors这个工具类里面封装了蛮多创建线程的方法,来简化我们平时对线程池的创建。
但是平时我们一般是不用Executors这个工具类来创建线程池的,因为Executors里面创建的线程池,参数设置得很不合理,直接使用可能会有很大问题。
4 小结
本节我们简单先回顾下我们平时线程池的使用,下节我们会开始看线程池的实现细节,有理解不对的地方欢迎指正哈。