共计 3590 个字符,预计需要花费 9 分钟才能阅读完成。
由于内容过多,分一个系列来写,这是第四篇。
九、Java 中的线程池
线程池用于异步或并发执行任务的场景,合理使用线程池带来的好处:
(1)降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
(2)提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行;
(3)提高线程的可管理性,使用线程池统一分配、调优和监控。
1、线程池的实现原理
当向线程池提交一个任务后,线程池的主要处理流程:
(1)线程池判断核心线程池里的线程是否都在执行任务,如果不是,那么创建一个新的工作线程来执行任务,如果核心线程池里的线程都在执行任务,那么进入下一步;
(2)线程池判断工作队列是否满了,如果工作队列没满,则将新提交的任务存储在这个工作队列中,如果满了,进行下一步;
(3)线程池判断线程池的线程是否都处于工作状态,如果不是,则创建一个新的工作线程来执行任务,如果满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor 执行 execute 方法分为下面四种情况:
(1)如果当前运行的线程少于 corePoolSize,那么创建新线程来执行任务 (执行这一步需要获得全局锁);
(2) 如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue;
(3)如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务 (执行这一步需要获取全局锁);
(4) 如果创建新线程将使当前运行的线程超过 maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。
注意:ThreadPoolExecutor 采取上述步骤的总体设计思路,是为了在执行 execute()方法时,尽可能的避免获取全局锁,ThreadPoolExecutor 在完成预热后 (当前运行的线程数大于等于 corePoolSize),几乎所有的 execute() 方法调用都在执行上面的步骤 2,步骤 2 是不需要获得全局锁的。
工作线程:线程池创建线程时,会把线程封装成工作线程 Worker,Worker 在执行完任务后,还会循环获取工作队列里的任务来执行。
2、线程池的创建
创建线程池的几个参数:
(1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于 corePoolSize 时就不在创建。调用 prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程;
(2)workQueue(任务队列):用于保存等待执行的任务的阻塞队列,比如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、
PriorityBlockingQueue;
(3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。注意:如果使用了无界的任务队列,这个参数就没效果了;
(4)threadFactory:用于创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字;
(5)handler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采用一种策略处理提交的新任务,默认的策略是 AbortPolicy——直接抛出异常,还有其他三种策略,分别是 CallerRunsPolicy——只用调用者所在线程来运行任务,DiscardPolicy——不处理,丢弃掉,DiscardOldestPolicy——丢弃队列里最前面
的任务,并执行当前任务;
(6)keepAliveTime(多余空闲线程的最长存活时间):当线程池中线程数量大于 corePoolSize,会根据 keepAliveTime 的值进行活性检查,一旦超时便销毁
大于 corePoolSize 小于等于 maximumPoolSize 的线程;
(7)unit:keepAliveTime 的时间单位。
3、向线程池提交任务
可以使用两个方法向线程池提交任务,分别是 execute()和 submit()方法。execute()用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功。submit()用于提交需要返回值的任务,线程池会返回一个 future 类型的对象,通过 future 对象可以判断任务是否执行成功。
4、关闭线程池
可以通过调用线程池里的 shutdown()或 shutdownNow()方法来关闭线程池。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor} finally {mainLock.unlock();
}
tryTerminate();}
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();} finally {mainLock.unlock();
}
tryTerminate();
return tasks;
}
5、合理分配线程池
根据不同任务特性,合理分配线程池,可以从这几个角度分析:任务的类型 (CPU 密集型、IO 密集型、混合型任务)、任务的优先级(高、中、低)、
任务的执行时间(长、中、短)、任务的依赖性(是否依赖其他系统资源,比如数据库连接)。
6、线程池的监控
对线程池进行监控,方便出问题时候,可以根据线程池的使用状况快速定位问题。
十、Executor 框架
1、Executor 框架简介
java 多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器 (Executor 框架) 将这些任务映射为固定数量的线程;在底层,操作系统内
核将这些线程映射到硬件处理器上。也就是应用程序通过 Executor 框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用
程序的控制。
Executor 框架主要由三大部分组成:
(1)任务 -> Runnable 接口、Callable 接口;
(2)任务的执行 -> Executor 接口、ExecutorService 接口、ThreadPoolExecutor 实现类、ScheduledThreadPoolExecutor 实现类;
(3)异步计算的结果 -> Future 接口、FutureTask 实现类。
ThreadPoolExecutor 通常使用工厂类 Executors 来创建,Executors 可以创建几种类型的 ThreadPoolExecutor;
(1)FixedThreadPool:适应于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适用于负载比较重的服务器;
(2)SingleThreadExecutor:适用于需要保证顺序地执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景;
(3)CachedThreadPool:是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器。
ScheduledThreadPoolExecutor 通常使用工厂类 Executors 来创建,适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要
限制后台线程的数量的应用场景。
FixedThreadPool 和 SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列,CachedThreadPool 使用没有容量的
SynchronousQueue 作为线程池的工作队列,但 CachedThreadPool 的 maximumPoolSize 是无界的,这意味着如果主线程提交任务的速度高于
maximumPoolSize 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程,会导致耗尽 CPU 和内存资源。SynchronousQueue 是一个
没有容量的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作。