一、线程池概述
1.1 什么是线程池?
- 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。
- 每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
- 如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。
- 如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
1.2 为什么要使用线程池?
- 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。
- 在
Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。 - 如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些"池化资源"技术产生的原因。比如大家所熟悉的数据库连接池正是遵循这一思想而产生的。
1.3 线程池的组成部分
- 线程池管理器(ThreadPoolManager):用于创建并管理线程池。
- 工作线程(WorkThread):线程池中线程。
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行。
- 任务队列:用于存放没有处理的任务。提供一种缓冲机制。
1.4 应用范围
- 需要大量的线程来完成任务,且完成任务的时间比较短。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
二、自定义线程池
- 示意图:
2.1 自定义拒绝策略接口
@FunctionalInterface
public interface RejectPolicy<T> {
/**
* 自定义的拒绝策略。
*
* @param queue 阻塞队列
* @param task 任务
*/
void reject(BlockingQueue<T> queue, T task);
}
2.2 自定义任务队列
@Slf4j
public class BlockingQueue<T> {
/* *
* 任务队列。
*/
private final Deque<T> queue = new ArrayDeque<>();
/* *
* 锁。
*/
private final ReentrantLock lock = new ReentrantLock();
/* *
* 生产者条件变量。
*/
private final Condition producerCondition = lock.newCondition();
/* *
* 消费者条件变量。
*/
private final Condition consumerCondition = lock.newCondition();
/* *
* 容量。
*/
private final int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
/**
* 阻塞获取。
*
* @return {@link T}
* @throws InterruptedException 中断异常
*/
public T take() throws InterruptedException {
// 上锁。
lock.lock();
try {
while (queue.isEmpty()) {
consumerCondition.await();
}
T t = queue.removeFirst();
producerCondition.signal();
return t;
} finally {
// 释放锁。
lock.unlock();
}
}
/**
* 带超时阻塞的获取方式。
*
* @param timeout 超时时间
* @param unit 单位
* @return {@link T}
* @throws InterruptedException 中断异常
*/
public T poll(long timeout, TimeUnit unit) throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
// 将超时时间转化为纳秒。
long nanos = unit.toNanos(timeout);
if (0 >= nanos) {
return null;
}
// 计算剩余时间。
nanos = consumerCondition.awaitNanos(nanos);
}
T t = queue.removeFirst();
producerCondition.signal();
return t;
} finally {
lock.unlock();
}
}
/**
* 阻塞添加。
*
* @param task 任务
* @throws InterruptedException 中断异常
*/
public void put(T task) throws InterruptedException {
lock.lock();
try {
while (capacity == queue.size()) {
log.debug("wait task join the queue...");
producerCondition.await();
}
queue.addLast(task);
log.debug("task is successfully added to the queue");
consumerCondition.signal();
} finally {
lock.unlock();
}
}
/**
* 带超时的阻塞添加。
*
* @param task 任务
* @param timeout 超时时间
* @param unit 单位
* @return boolean
* @throws InterruptedException 中断异常
*/
public boolean offer(T task, long timeout, TimeUnit unit) throws InterruptedException {
lock.lock();
try {
while (capacity == queue.size()) {
long nanos = unit.toNanos(timeout);
if (0 >= nanos) {
return false;
}
log.debug("wait task join the queue...");
nanos = producerCondition.awaitNanos(nanos);
}
queue.addLast(task);
log.debug("task is successfully added to the queue");
consumerCondition.signal();
return true;
} finally {
lock.unlock();
}
}
/**
* 同步获取队列大小。
*
* @return int
*/
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
/**
* 尝试添加任务。
*
* @param policy 自定义的拒绝策略
* @param task 任务
*/
public void tryPut(RejectPolicy<T> policy, T task) {
lock.lock();
try {
// 队列满了则触发拒绝策略。
while (capacity == queue.size()) {
policy.reject(this, task);
}
queue.addLast(task);
log.debug("task is successfully added to the queue");
consumerCondition.signal();
} finally {
lock.unlock();
}
}
}
2.3 自定义线程池
@Slf4j
public class ThreadPool {
/* *
* 任务阻塞队列。
*/
private final BlockingQueue<Runnable> taskQueue;
/* *
* 核心线程数。
*/
private final int coreSize;
/* *
* 工作线程集。
*/
private final HashSet<Worker> workers = new HashSet<>();
/* *
* 超时时间。
*/
private final long timeout;
/* *
* 时间单位。
*/
private final TimeUnit timeUnit;
/* *
* 拒绝策略。
*/
private final RejectPolicy<Runnable> rejectPolicy;
/**
* 工作线程。
**/
@EqualsAndHashCode(callSuper = false)
private class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
try {
Runnable pollTask = taskQueue.poll(timeout, timeUnit);
// 当任务不为空,则执行任务。
while (null != task || null != pollTask) {
log.debug("task:[{}] is running...", task);
task.run();
}
synchronized (workers) {
log.debug("worker:[{}] is removed", this);
workers.remove(this);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 当 task 执行完毕,再接着从任务队列获取任务并执行。
task = null;
}
}
}
/**
* 线程池构造器。
*
* @param taskQueue 任务队列
* @param coreSize 线程核心数
* @param timeout 超时时间
* @param timeUnit 时间单位
* @param rejectPolicy 拒绝策略
*/
public ThreadPool(
BlockingQueue<Runnable> taskQueue, int coreSize,
long timeout, TimeUnit timeUnit,
RejectPolicy<Runnable> rejectPolicy) {
this.taskQueue = taskQueue;
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.rejectPolicy = rejectPolicy;
}
/**
* 执行任务。
*
* @param task 任务
*/
public void execute(Runnable task) {
synchronized (workers) {
// 如果工作线程数没有超过核心线程,则正常工作。
if (coreSize > workers.size()) {
Worker worker = new Worker(task);
log.debug("add worker{}, task={}", worker, task);
workers.add(worker);
worker.start();
} else {
// 注意:这里可以使用以下方式进行处理:
// 方式一:死等。
// 方式二:带超时等待。
// 方式三:让调用者放弃任务执行。
// 方式四:让调用者抛出异常。
// 方式五:让调用者自己执行任务。
taskQueue.tryPut(rejectPolicy, task);
}
}
}
}
2.4 测试
@Slf4j
public class ThreadPoolTests {
public static void main(String[] args) {
// 构建容量为1的阻塞队列。
BlockingQueue<Runnable> taskQueue = new BlockingQueue<>(1);
// 核心线程数及超时时间。
int coreSize = 1;
long timeOut = 1L;
// 构造线程池。
ThreadPool threadPool = new ThreadPool(
taskQueue,
coreSize,
timeOut,
TimeUnit.SECONDS,
(queue, task) -> {
/* *
* 使用拒绝策略一:死等: queue.put(task);
* 使用拒绝策略二:带超时等待 queue.offer(task, 1500, TimeUnit.MILLISECONDS);
* 使用拒绝策略三:让调用者放弃任务执行 log.debug("放弃{}", task);
* 使用拒绝策略四:让调用者抛出异常 throw new RuntimeException("任务执行失败 " + task);
* 使用拒绝策略五:让调用者自己执行任务(此处使用)。
*/
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}
三、Executors
Executors类(静态Executor工厂)用于创建线程池,属于java.util.concurrent包,它有创建 6 种类型线程池的方法。
3.1 newFixedThreadPool
固定大小的线程池。
- 方法源码:
/**
* Creates a thread pool that reuses a fixed number of threads
* operating off a shared unbounded queue. At any point, at most
* {@code nThreads} threads will be active processing tasks.
* If additional tasks are submitted when all threads are active,
* they will wait in the queue until a thread is available.
* If any thread terminates due to a failure during execution
* prior to shutdown, a new one will take its place if needed to
* execute subsequent tasks. The threads in the pool will exist
* until it is explicitly {@link ExecutorService#shutdown shutdown}.
*
* @param nThreads the number of threads in the pool
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
特点:
- 核心线程数
==最大线程数(没有救急线程被创建),因此也无需超时时间。 - 阻塞队列是无界的,可以放任意数量的任务。
- 核心线程数
-
适用场景:任务量已知,相对耗时的任务。
3.2 newCachedThreadPool
缓存(可伸缩的)线程池。
- 方法源码:
/**
* Creates a thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are
* available. These pools will typically improve the performance
* of programs that execute many short-lived asynchronous tasks.
* Calls to {@code execute} will reuse previously constructed
* threads if available. If no existing thread is available, a new
* thread will be created and added to the pool. Threads that have
* not been used for sixty seconds are terminated and removed from
* the cache. Thus, a pool that remains idle for long enough will
* not consume any resources. Note that pools with similar
* properties but different details (for example, timeout parameters)
* may be created using {@link ThreadPoolExecutor} constructors.
*
* @return the newly created thread pool
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
-
特点:
-
核心线程数是 0, 最大线程数是
Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着:- 全部都是救急线程(60s 后可以回收);
- 救急线程可以无限创建。
-
队列采用了
SynchronousQueue实现特点是,它没有容量,没有线程来取是放不进去的(相当于“一手交钱,一手交货”)。
-
-
适用场景:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况。
3.3 newSingleThreadExecutor
单线程执行器。
- 方法源码:
/**
* Creates an Executor that uses a single worker thread operating
* off an unbounded queue. (Note however that if this single
* thread terminates due to a failure during execution prior to
* shutdown, a new one will take its place if needed to execute
* subsequent tasks.) Tasks are guaranteed to execute
* sequentially, and no more than one task will be active at any
* given time. Unlike the otherwise equivalent
* {@code newFixedThreadPool(1)} the returned executor is
* guaranteed not to be reconfigurable to use additional threads.
*
* @return the newly created single-threaded Executor
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
-
适用场景:
- 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
-
问题1:既然是单线程执行,那么它与直接创建一个线程来执行任务有何区别呢?
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。
-
问题2:该单线程执行器,与固定线程池
newFixedThreadPool设置值为1又有何区别呢?Executors.newSingleThreadExecutor()线程个数始终为1,不能修改。(FinalizableDelegatedExecutorService应用的是装饰器模式,只对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法。)Executors.newFixedThreadPool(1)初始时为1,以后还可以修改。(对外暴露的是ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改。)
3.4 其它创建线程方法
Executors.newScheduledThreadPool:创建⼀个可以执行延迟任务的线程池。---> 本章第八节有该方法的使用方式说明。Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定),JDK1.8 添加。
四、ThreadPoolExecutor
4.1 类图与继承关系
此处为
JDK11版本的截图。
4.2 线程池状态
| 状态名 | 高3位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
|---|---|---|---|---|
| RUNNING | 111 | Y | Y | / |
| SHUTDOWN | 000 | N | Y | 不会接收新任务,但会处理阻塞队列剩余任务 |
| STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列任务 |
| TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 |
| TERMINATED | 011 | - | - | 终结状态 |
- 从数字上比较:
TERMINATED>TIDYING>STOP>SHUTDOWN>RUNNING - 这些信息存储在一个原子变量
ctl中,目的是将线程池状态与线程个数合二为一,这样就可以用一次cas原子操作进行赋值。 - 代码说明:
// c 为旧值, ctlOf 返回结果为新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }
4.3 构造方法及参数说明
- 构造方法之一(源码) :
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
-
参数说明:
corePoolSize:核心线程数目 (最多保留的线程数)。maximumPoolSize:最大线程数目。keepAliveTime:生存时间 - 针对救急线程。unit:时间单位 - 针对救急线程。workQueue:阻塞队列。threadFactory:线程工厂 - 可以为线程创建时起个好名字。handler:拒绝策略。
-
工作方式:
-
线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
-
当线程数达到
corePoolSize并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue队列排队,直到有空闲的线程。 -
如果队列选择了有界队列,那么任务超过了队列大小时,会创建 (
maximumPoolSize-corePoolSize) 数目的线程来救急。 -
如果线程到达
maximumPoolSize仍然有新任务这时会执行拒绝策略。拒绝策略JDK提供了 4 种实现,其它著名框架也提供了实现:AbortPolicy让调用者抛出RejectedExecutionException异常,这是默认策略。CallerRunsPolicy让调用者运行任务。DiscardPolicy放弃本次任务。DiscardOldestPolicy放弃队列中最早的任务,本任务取而代之。Dubbo的实现,在抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题。Netty的实现,是创建一个新线程来执行任务。ActiveMQ的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略。PinPoint的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略。
-
当高峰过去后,超过
corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime和unit来控制。
4.4 提交任务
- 方法及说明:
// 执行任务。
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果。
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消。
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间。
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
4.5 关闭线程池
- 方法及说明:
/* *
* 线程池状态变为 SHUTDOWN;
* 不会接收新任务;
* 但已提交任务会执行完;
* 此方法不会阻塞调用线程的执行。
*/
void shutdown();
/* *
* 线程池状态变为 STOP;
* 不会接收新任务;
* 会将队列中的任务返回;
* 并用 interrupt 的方式中断正在执行的任务。
*/
List<Runnable> shutdownNow();
- 其它方法:
// 不在 RUNNING 状态的线程池,此方法就返回 true。
boolean isShutdown();
// 线程池状态是否是 TERMINATED。
boolean isTerminated();
// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待。
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
五、Executors 与 ThreadPoolExecutor
线程池
ThreadPoolExecutor和 线程池工厂类Executors。
5.1 异同点
Executors类和ThreadPoolExecutor都是java.util.concurrent并发包下面的类。Executos下面的newFixedThreadPool()、newScheduledThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()底线的实现都是用的ThreadPoolExecutor实现的,所以ThreadPoolExecutor更加灵活。
5.2 *规范建议
- 摘自《阿里巴巴java开发手册》 :
-
原因说明:
JDK中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活。- 前面几种方法内部也是通过
ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
六、正确处理异常
6.1 主动捕获异常
- 代码示例:
@Slf4j
public class HandleExceptionSample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
try {
log.debug("running...");
int i = 1 / 0;
} catch (Exception e) {
log.error("error info:[{}]", e.getMessage());
}
// running...
// error info:[/ by zero]
});
}
}
6.2 使用 Future
- 代码示例:
@Slf4j
public class HandleExceptionSample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Boolean> f = executor.submit(() -> {
log.debug("running...");
int i = 1 / 0;
return true;
});
log.debug("result:[{}]", f.get());
// running...
// java.lang.ArithmeticException: / by zero
}
}
七、异步模式之工作线程
7.1 概述
- 定义:让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。
- 举例:餐馆的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)。
- 注意:不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。(例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工)。
7.2 饥饿问题
- 固定大小线程池会有饥饿现象。
- 示意图:
- 两个工人相当于是同一个线程池中的两个线程。
- 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作。比如工人1处理了点餐任务,接下来它要等着 工人2把菜做好,这个流程就算完整的结束。但现在同时来了两个客人,这个时候工人1和工人2都去处理点餐了,这时没人做饭了,此时就发生了饥饿现象。
- 代码示例:
@Slf4j
public class ThreadHungrySample {
private static final List<String> MENUS = Arrays.asList("回锅肉", "番茄炒蛋", "宫保鸡丁");
private static final SecureRandom RANDOM = new SecureRandom();
private static String cookie() {
int r = RANDOM.nextInt(MENUS.size());
return MENUS.get(r);
}
public static void main(String[] args) {
// 手动创建线程池:
// 核心线程数、最大线程数为2,存活时间为3s,阻塞队列大小为2;
// 使用默认线程工厂命名;
// 使用抛出异常策略。
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
2,
3,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 创建两个相同的线程池,模拟饥饿现象。
for (int i = 0; i < 2; i++) {
pool.execute(() -> {
log.debug("[{}] 处理点餐...", Thread.currentThread().getName());
Future<String> future = pool.submit(() -> {
log.debug("点菜");
return cookie();
});
try {
log.debug("上菜:[{}]", future.get());
} catch (ExecutionException | InterruptedException e) {
log.error("error:[{}]", e.getMessage());
Thread.currentThread().interrupt();
}
});
}
// [pool-1-thread-1] 处理点餐...
// [pool-1-thread-2] 处理点餐...
}
}
- 解决方式(不同任务类型,采用不同的线程池) :
@Slf4j
public class ThreadHungrySample {
private static final List<String> MENUS = Arrays.asList("回锅肉", "番茄炒蛋", "宫保鸡丁");
private static final SecureRandom RANDOM = new SecureRandom();
private static String cookie() {
int r = RANDOM.nextInt(MENUS.size());
return MENUS.get(r);
}
public static void main(String[] args) {
// 1.专门处理『点餐任务』的线程池。
ThreadPoolExecutor waiterPool = new ThreadPoolExecutor(
2,
2,
3,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 2.专门处理『做菜任务』的线程池。
ThreadPoolExecutor cookPool = new ThreadPoolExecutor(
2,
2,
3,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 通过不同线程池执行不同任务。
for (int i = 0; i < 2; i++) {
waiterPool.execute(() -> {
log.debug("[{}] 处理点餐...", Thread.currentThread().getName());
Future<String> future = cookPool.submit(() -> {
log.debug("点菜");
return cookie();
});
try {
log.debug("上菜:[{}]", future.get());
} catch (ExecutionException | InterruptedException e) {
log.error("error:[{}]", e.getMessage());
Thread.currentThread().interrupt();
}
});
}
// [pool-1-thread-1] 处理点餐...
// [pool-1-thread-2] 处理点餐...
// 点菜
// 点菜
// 上菜:[回锅肉]
// 上菜:[番茄炒蛋]
}
}
7.3 线程池的伸缩性对性能的影响
- 创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
- 销毁太多线程,将导致之后浪费时间再次创建它们。
- 创建线程太慢,将会导致长时间的等待,性能变差。
- 销毁线程太慢,导致其它线程资源饥饿。
7.4 CPU 密集型运算
- 通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证
cpu时钟周期不被浪费。
7.5 I/O 密集型运算
cpu不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用cpu资源;但当你执行I/O操作时、远程rpc调用时,包括进行数据库操作时,这时候cpu就闲下来了,你可以利用多线程提高它的利用率。经验公式如下:
-
线程数 = 核数 * 期望 cpu 利用率 * 总时间( cpu 计算时间+等待时间)/ cpu 计算时间
- 例如 4 核
cpu计算时间是 50% ,其它等待时间是 50%,期望cpu被 100% 利用,套用公式:4 * 100% * 100% / 50% = 8 - 例如 4 核
cpu计算时间是 10% ,其它等待时间是 90%,期望cpu被 100% 利用,套用公式:4 * 100% * 100% / 10% = 40
- 例如 4 核
八、任务调度线程池
8.1 Timer
在『任务调度线程池』功能加入之前,可以使用
java.util.Timer来实现定时功能,Timer的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
- 代码示例:
@Slf4j
public class TimerSample {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("run task1...");
log.error("task1 error");
int i = 1 / 0;
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("run task2...");
}
};
// 使用 timer 添加两个任务,希望它们都在 1s 后执行。
// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的异常,影响了『任务2』的执行。
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
// run task1...
// task1 error
// java.lang.ArithmeticException: / by zero
}
}
8.2 ScheduledExecutorService
整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务。
- 代码示例:
@Slf4j
public class ScheduledExecutorServiceSample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 添加两个任务,希望它们都在 1s 后执行。
executor.schedule(() -> {
log.debug("任务1,执行时:[{}]", new Date());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
log.error(e.getMessage());
Thread.currentThread().interrupt();
}
}, 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> log.debug("任务2,执行时:[{}]", new Date()), 1000, TimeUnit.MILLISECONDS);
// 任务1,执行时:[Thu Sep 22 09:00:46 CST 2022]
// 任务2,执行时:[Thu Sep 22 09:00:46 CST 2022]
}
}
- 其他方法补充:
不管是
scheduleAtFixedRate()还是scheduleWithFixedDealy()都会等待上一个任务运行结束再进行下一个任务。如果需要并行执行,可以考虑任务中使用异步处理,比如Spring Boot中的@Async。
| 方法签名 | 方法说明 | 备注 |
|---|---|---|
scheduleAtFixedRate(Runnable command, long initialDelay,long period,TimeUnit unit); | 以固定比率执行:如果上一个任务的执行时间大于等待时间,任务结束后,下一个任务马上执行。 | scheduleAtFixedRate() 是从任务开始时算起。 |
scheduleWithFixedDelay(Runnable command, long initialDelay,long delay,TimeUnit unit); | 以固定的延迟来执行:如果上个任务的执行时间大于等待时间,任务结束后也会等待相应的时间才执行下一个任务。 | scheduleWithFixedDelay() 是从任务结束时算起。 |
8.3 定时任务的实践
需求:如何让每周四 18:00:00 定时执行任务?
- 代码示例:
@Slf4j
public class ScheduledTasksSample {
public static void main(String[] args) {
// 当前时间。
LocalDateTime now = LocalDateTime.now();
// 获取本周四时间。
LocalDateTime thursday = now.
with(DayOfWeek.THURSDAY).
withHour(18).
withMinute(0).
withSecond(0).
withNano(0);
// 如果已经过了本周四,就获取下周四的时间。
if (now.compareTo(thursday) >= 0) {
thursday = thursday.plusWeeks(1);
}
// 计算延时执行时间(即:当前与周四的时间差)。
long initialDelay = Duration.between(now, thursday).toMillis();
// 计算间隔时间(即:1周隔了多少毫秒)。
long oneWeek = 7 * 24 * 60 * 60 * 1000L;
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
log.debug("开始时间:[{}]", new Date());
executor.scheduleAtFixedRate(() ->
log.debug("执行时间:[{}]", new Date()), initialDelay, oneWeek, TimeUnit.MILLISECONDS);
}
}
九、Tomcat 线程池
9.1 概述
Tomcat 在哪里用到了线程池呢?
LimitLatch用来限流,可以控制最大连接个数,类似JUC中的Semaphore。Acceptor只负责接收新的socket连接。Poller只负责监听socketchannel 是否有可读的I/O事件,一旦可读,封装一个任务对象(socketProcessor),提交给Executor线程池处理。Executor线程池中的工作线程最终负责处理请求。
与
ThreadPoolExecutor的不同之处:
- 如果总线程数达到
maximumPoolSize:这时不会立刻抛RejectedExecutionException异常; - 而是再次尝试将任务放入队列,如果还失败,才抛出
RejectedExecutionException异常。
9.2 Connector 配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
acceptorThreadCount | 1 | acceptor 线程数量 |
pollerThreadCount | 1 | poller 线程数量 |
minSpareThreads | 10 | 核心线程数,即 corePoolSize |
maxThreads | 200 | 最大线程数,即 maximumPoolSize |
executor | - | Executor 名称,用来引用下面的 Executor |
9.3 Executor 线程配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
threadPriority | 5 | 线程优先级 |
daemon | true | 是否守护线程 |
minSpareThreads | 25 | 核心线程数,即 corePoolSize |
maxThreads | 200 | 最大线程数,即 maximumPoolSize |
maxIdleTime | 60000 | 线程生存时间,单位是毫秒,默认值即 1 分钟 |
maxQueueSize | Integer.MAX_VALUE | 队列长度 |
prestartminSpareThreads | false | 核心线程是否在服务器启动时启动 |
- 示意图:
十、ForkJoinPool
Fork/Join是JDK1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算。
10.1 概述
- 所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解。
Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率。Fork/Join默认会创建与cpu核心数大小相同的线程池。
10.2 使用
前提:提交给
Fork/Join线程池的任务需要继承RecursiveTask(有返回值)或RecursiveAction(没有返回值)需求:通过任务拆分,对 1~n 进行整数求和。
- 代码示例:
@Slf4j
public class ForkJoinPoolSample extends RecursiveTask<Integer> {
private final int begin;
private final int end;
public ForkJoinPoolSample(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
// 开始和结束都为相同数就没必要进行拆分。
if (begin == end) {
return begin;
}
// 相邻的数,直接返回求和结果,其目的是减少拆分次数。
if ((end - begin) == 1) {
return end + begin;
}
// 中间进行拆任务,一共拆成两个任务。
int mid = (end + begin) / 2;
// 开始到中间。
ForkJoinPoolSample task1 = new ForkJoinPoolSample(begin, mid);
task1.fork();
// 中间到结束。
ForkJoinPoolSample task2 = new ForkJoinPoolSample(mid + 1, end);
task2.fork();
// 合并结果。
int result = task1.join() + task2.join();
log.debug("join: {} + {} = {}", task1, task2, result);
return result;
}
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
Integer result = pool.invoke(new ForkJoinPoolSample(1, 10));
log.debug("result=[{}]", result);
// join: {1,2} + {3,3} = 6
// join: {1,3} + {4,5} = 15
// join: {6,7} + {8,8} = 21
// join: {6,8} + {9,10} = 40
// join: {1,5} + {6,10} = 55
// result=[55]
}
}
十一、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。