写在前面:
文章内容是通过个人整理以及参考相关资料总结而出,难免会出现部分错误
如果出现错误,烦请在评论中指出!谢谢
9 BlockingQueue阻塞队列
一般情况下线程不阻塞肯定比较好,但是某种情况下不得不阻塞,必须要阻塞
例如当火锅店人特别多的时候,后面的人就必须要排队等待叫号,那么队伍就可以看做阻塞队列
当队列为空时,从队列中获取元素的操作将会被阻塞
当队列为满时,从队列中添加元素的操作将会被阻塞
类似于生产者消费者模型
用处:
在多线程领域:
所谓阻塞,在某些情况下会`挂起`线程;一旦条件满足,被阻塞的线程又被重新唤醒(比如wait和notify方法)
为什么需要BlockingQueue:
不需要关心什么时候需要阻塞线程,什么时候唤醒线程,BlockingQueue会进行操作
9.1 继承树
在Collection接口下Queue接口和List、Set接口平级,而BlockingQueue接口继承于Queue接口
核心实现类:
ArrayBlockingQueue:由数组构成的
有界
阻塞队列 LinkedBlockingQueue:由链表组成的
有界
(但大小默认值为integer.MAX_VALUE)阻塞队列 SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
9.2 API
构造器
/**
* Creates an {@code ArrayBlockingQueue} with the given (fixed)
* capacity and default access policy.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity < 1}
*/
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
通过传入一个int参数capacity来指定阻塞队列的大小,从而说明了ArrayBlockingQueue是一个有界的阻塞队列
add()
向阻塞队列中添加元素,当阻塞队列满时会抛出IllegalStateException,提示Queue full
演示:
public class BolcokingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
System.out.println(queue.add("x"));
}
}
结果:
true
true
true
Exception in thread "main" java.lang.IllegalStateException: Queue full
当没有超出队列长度时,则返回布尔值,表示插入是否成功
remove()
从阻塞队列中移除元素,当阻塞队列为空时,再次移除会抛出NoSuchElementException
演示:
public class BolcokingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
}
}
结果:
a
b
c
Exception in thread "main" java.util.NoSuchElementException
从出队列的顺序也可以看出队列的先进先出
element()
不会从队列中取出元素,而只是检查队列的头元素,当队列为空时,会抛出NoSuchElementException
演示:
public class BolcokingQueueDemo {
public static void main(String[] args) {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
System.out.println(queue.add("a"));
System.out.println(queue.add("b"));
System.out.println(queue.add("c"));
System.out.println(queue.element());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.remove());
System.out.println(queue.element());
}
}
结果:
a
a
b
c
Exception in thread "main" java.util.NoSuchElementException
offer()
向队列中插入元素,当队列为满时并不会抛出异常,而只是返回false
poll()
从队列中取出元素,当队列为空时也不会抛出异常,而只是返回null
peek()
不会从队列中取出元素,而只是检查队列的头元素;当队列为空时不会抛出异常,而只是返回null
put()
向队列中插入元素,当队列为满时不会抛出异常,也不会返回null;而是处于阻塞状态
public class BolcokingQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
queue.put("a");
queue.put("a");
queue.put("a");
queue.put("a");
}
}
程序一直在执行,说明处于阻塞状态
take()
从队列中取出元素,当队列为空时不会抛出异常,也不会返回null;而是处于阻塞状态
offer(e,time,unit)
向队列中插入元素,当队列为满时不会抛出异常,也不会返回null,而是处于阻塞状态;然而这种阻塞状态具有时间限制,当超时时程序就会结束,也不会抛出异常,没有任何返回
public class BolcokingQueueDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
queue.offer("a");
queue.offer("a");
queue.offer("a");
queue.offer("a",3,TimeUnit.SECONDS);
}
}
结果:
poll(e,time,unit)
从队列中取出元素,当队列为空时不会抛出异常,也不会返回null,而是处于阻塞状态;然而这种阻塞状态具有时间限制,当超时时程序就会结束,也不会抛出异常,没有任何返回
11 线程池
假设在Web环境下一个请求传递过来就创建一个线程用于处理该请求,那么对系统造成巨大的压力,而且当任务处理完成之后就销毁该线程,那么也造成了一定CPU资源的浪费
假如说在一瞬间传递过来许多请求,那么就需要创建许多线程来对请求进行处理,可能会造成应用的瘫痪
因此我们需要线程池来对线程进行管理以及线程的复用
上图对线程池中重要的角色进行了表述,阻塞队列用于接收请求,而线程池中的线程通过获取阻塞队列中的任务来对任务进行处理
11.1 自定义线程池
通过上面对于线程池的分析,我们对线程池中关键的组件进行自定义
11.1.1 自定义阻塞队列
public class BlockingQueue<T> {
// 任务队列
private Deque<T> queue = new ArrayDeque<>();
// 锁
private ReentrantLock lock = new ReentrantLock();
// 生产任务条件变量
private Condition fullWaitSet = lock.newCondition();
// 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 容量
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
// 阻塞获取任务
public T take() {
lock.lock();
try {
while (queue.isEmpty()) {
try {
emptyWaitSet.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T task = queue.removeFirst();
return task;
} finally {
lock.unlock();
}
}
// 阻塞添加任务
public void put(T element) {
lock.lock();
try {
while (queue.size() ==capacity) {
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(element);
emptyWaitSet.signalAll();
} finally {
lock.unlock();
}
}
// 指定超时时间获取任务
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 统一时间管理
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
if (nanos < 0) {
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T task = queue.removeFirst();
fullWaitSet.signalAll();
return task;
} finally {
lock.unlock();
}
}
// 获取当前任务队列大小
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
}
首先分析下自定义阻塞队列中的属性:
queue
实际上就是阻塞队列中保存的任务队列,这里使用数组实现的双向链表lock
就是在获取任务和添加任务时使用的锁对象,因为获取和添加时需要保证线程安全fullWaitSet
就是当阻塞队列中任务满了,依然有线程来添加任务时,则加入到该WaitSet休息室中emptyWaitSet
就是当任务队列中任务为空时,依然有线程来获取任务时,则加入到该WaitSet休息室中capacity
就是阻塞队列的容量大小
接下来看下获取任务的方法:
-
首先对该方法加锁之后,判断任务队列是否为空,如果为空就添加到emptyWaitSet休息室中,
这里使用while进行判断,就是当线程被唤醒之后依然需要判断任务队列是否为空
-
如果不为空,则从双向链表中获取第一个任务并返回,同时因为这里已经消费了一个任务,需要唤醒fullWaitSet休息室中的线程
下面就来分析下添加任务的方法:
- 基本思路和获取任务相同
最后分析下带超时时间的获取任务方法:
-
这里首先通过
unit.toNanos(timeout)
方法将时间统一转化为微秒级别,便于管理 -
判断队列是否为空,如果为空,就调用
emptyWaitSet.awaitNanos(nanos)
阻塞当前线程,这里返回的就是定义的超时时间和已经阻塞时间差,也就是剩余的阻塞时间
从注释中就可以看出返回值就是传入的参数减去阻塞耗费的时间
-
我们知道在
await()
阻塞时间未到时可能被其他线程唤醒继续执行,当下一次进行循环时,就需要继续使用剩余的阻塞时间作为新的阻塞时间 -
如果剩余阻塞时间小于0时就直接返回null
-
如果队列不为空时就返回头部的任务并唤醒fullWaitSet休息室中的所有线程
11.1.2 自定义线程池
public class ThreadPoll {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务的超时时间
private long timeout;
// 时间单位
private TimeUnit unit;
public ThreadPoll(int coreSize, long timeout, TimeUnit unit, int queueCapacity) {
this.coreSize = coreSize;
this.timeout = timeout;
this.unit = unit;
this.taskQueue = new BlockingQueue(queueCapacity);
}
// 执行任务
public void execute(Runnable task) {
// 当任务数没有超过核心线程数时,直接交给worker线程处理
// 如果任务数超过核心线程数,就放入阻塞队列中
synchronized (workers) {
if (workers.size() <= coreSize) {
Worker worker = new Worker(task);
log.info("新增worker: {}",worker);
workers.add(worker);
worker.start();
} else {
log.info("加入任务队列: {}",task);
taskQueue.put(task);
}
}
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 如果任务不为空就直接执行
// 当任务执行完毕不能销毁线程,而是从任务队列中继续获取任务
while (task != null || (task = taskQueue.take()) != null) {
try {
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
workers.remove(this);
}
}
}
}
我们首先对线程池中维护自定义Worker类进行分析:
- 为了实现线程的复用,这里巧妙的向线程类中维护了一个task对象,当task对象执行完毕就继续从任务队列中获取并更新task任务
- 由于Worker类继承于Thread类,因此重写的
run()
方法实际上就是循环判断任务是否为空,如果不为空,就调用任务对象的run()
方法 - 然后再第二次判断时从任务队列中获取新的任务
接着来分析下线程池中的属性:
taskQueue
就是任务队列,用来封装当任务数超过核心线程数之后存储任务workers
中封装的就是实际执行的线程集合- 下面的核心线程数、超时时间就不一一进行分析
最后来分析下执行任务的方法:
- 这里首先对线程集合进行加锁,由于一个线程内部封装了一个任务,因此比较线程数和核心线程数的大小实际上就是比较任务数和核心线程数之间的大小
- 如果任务数没有超过核心线程数,就创建一个新的线程对象,并且将当前的任务存储到线程中,然后执行该任务
- 如果任务数超过核心线程数,就放入到阻塞队列中
11.1.3 测试自定义线程池
测试类
@Slf4j
public class TestPool {
public static void main(String[] args) {
ThreadPoll pool = new ThreadPoll(2, 1000, TimeUnit.MILLISECONDS, 10);
for (int i = 0; i < 5; i++) {
int j = i;
pool.execute(() -> {
log.info("{}",j);
});
}
}
}
这里定义了5个任务,然后在核心线程数为2的线程池中进行执行
测试结果
这里可以发现一开始线程池中新增加了两个worker,然后剩下的任务都加入到阻塞队列中,当worker线程执行完毕后就获取新的任务并执行
然而这里出现了一个问题:我们发现应用并没有停止,那么这是为什么呢?
实际上因为当执行到(task = taskQueue.take()) != null
这时就会处于阻塞状态没办法继续执行
11.1.4 设置线程等待任务超时时间
刚才我们使用take()
来获取任务,当任务队列中没有任务时就会一直阻塞
但实际上我们之前已经定义了poll()
来获取指定超时时间的任务
那么下面就仅仅对Worker#run()
方法做些改变:
@Override
public void run() {
// 如果任务不为空就直接执行
// 当任务执行完毕不能销毁线程,而是从任务队列中继续获取任务
while (task != null || (task = taskQueue.poll(timeout,unit)) != null) {
try {
log.info("正在执行{}...",task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
log.info("移除worker:{}",this);
workers.remove(this);
}
}
测试结果
再次执行发现等待1s之后就结束了应用,且worker线程已经被移除
11.1.5 测试任务数超过核心线程数+任务队列大小
当任务数超过核心线程数+任务队列大小之后,说明当前的核心线程都在执行,且任务队列当中无法放入任务,那么会发生什么情况呢?
我们这里先对测试类做一些修改:
@Slf4j
public class TestPool {
public static void main(String[] args) {
ThreadPoll pool = new ThreadPoll(2, 1000, TimeUnit.MILLISECONDS, 5);
for (int i = 0; i < 5; i++) {
int j = i;
pool.execute(() -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("{}",j);
});
}
}
}
这里为每个任务增加休眠时间的原因就是防止线程很快执行完一个任务继续执行其他任务
测试结果
因为这里我们设置线程核心数为2,阻塞队列大小为5,而任务数为10,所以现在核心线程和阻塞队列可以容纳的任务数为7,然而这里只有1个线程在等待加入
因为我们定义阻塞队列的添加任务方法时是阻塞的,所以某个线程提交任务并阻塞之后,其他线程就无法再次提交任务
所以我们需要对等待的任务指定不同的拒绝策略,来决定等待的线程应该如何被拒绝
11.1.6 自定义拒绝策略
我们这里首先要在自定义阻塞队列中添加一个带超时时间的添加任务方法
// 指定超时时间获取任务
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 统一时间管理
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
if (nanos < 0) {
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T task = queue.removeFirst();
fullWaitSet.signalAll();
return task;
} finally {
lock.unlock();
}
}
对于不同的线程池可能需要实现不同的拒绝策略,因此我们需要抽象出一个接口供子类来实现(策略模式)
@FunctionalInterface
public interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue,T task);
}
我们思考下,在指定拒绝策略时,我们必须要知道我们操作的是什么样的阻塞队列以及什么样的任务
我们在自定义阻塞队列时使用了泛型来对不同的任务进行处理,因此这里也用泛型且和阻塞队列、任务的泛型一致
既然我们定义了拒绝策略,就需要在自定义阻塞队列类中添加对于阻塞队列已满,通过拒绝策略来执行添加的方法
// 添加任务,但当阻塞队列满时执行拒绝策略
public void tryPut(RejectPolicy<T> rejectPolicy,T element) {
lock.lock();
try {
if (queue.size() == capacity) {
rejectPolicy.reject(this,element);
} else {
queue.addLast(element);
log.info("加入任务队列:{}",element);
emptyWaitSet.signalAll();
}
} finally {
lock.unlock();
}
}
修改自定义线程池,向线程池中添加拒绝策略对象属性,然后在execute()
执行任务方法中当任务数超过核心线程数时执行阻塞队列的tryPut()方法
(带有拒绝策略的添加方法)
@Slf4j
public class ThreadPoll {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务的超时时间
private long timeout;
// 时间单位
private TimeUnit unit;
// 拒绝策略
private RejectPolicy<Runnable> rejectPolicy;
public ThreadPoll(int coreSize, long timeout, TimeUnit unit, int queueCapacity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.unit = unit;
this.taskQueue = new BlockingQueue(queueCapacity);
this.rejectPolicy = rejectPolicy;
}
// 执行任务
public void execute(Runnable task) {
// 当任务数没有超过核心线程数时,直接交给worker线程处理
// 如果任务数超过核心线程数,就放入阻塞队列中
synchronized (workers) {
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
log.info("新增worker:{},task:{}",worker,task);
workers.add(worker);
worker.start();
} else {
taskQueue.tryPut(rejectPolicy,task);
}
}
}
这里将拒绝策略对象添加到自定义线程池类的属性中,然后通过构造器来指定不同的拒绝策略
修改测试类-放弃执行策略
@Slf4j
public class TestPool {
public static void main(String[] args) {
ThreadPoll pool = new ThreadPoll(2, 1000, TimeUnit.MILLISECONDS, 5,(queue,task) -> {
log.info("队列已满,放弃任务{}",task);
});
for (int i = 0; i < 10; i++) {
int j = i;
pool.execute(() -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("{}",j);
});
}
}
}
我们知道当任务数超过核心数之后,就会调用阻塞队列的tryPut()方法,而tryPut()方法中当阻塞队列已满时实际上就是我们定义的拒绝策略方法
测试结果
11.2 ThreadPoolExecutor
11.2.1 Executor
这里我们首先看下Executor接口的继承体系:
接着来看下源码中对于该接口的描述:
An object that executes submitted Runnable tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. An Executor is normally used instead of explicitly creating threads. For example, rather than invoking new Thread(new(RunnableTask())).start() for each of a set of tasks, you might use:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...
从官网的解释可以看出,Executor是用来执行提交的Runnable任务,也就是线程池的顶级接口
11.2.2 ExecutorService
实际使用的线程池接口(相对于Executor接口提供了更多的方法)
11.2.3 ThreadPoolExecutor
创建线程池对象使用的实现类
11.2.4 Executors
Executor的工具类(工厂类),用来生成ThreadPoolExecutor对象
11.3 创建线程池
11.3.1 Executors.newFixedThreadPool
创建指定线程数量的线程池,池中的线程数量不会发生改变;适用于执行长期任务
测试:
public class ExectorDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
try {
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
}
线程池调用execute方法,里面传入Runnable接口实例作为执行任务,线程池取出线程来执行对应的任务
线程池适用完毕需要调用shutdown方法关闭线程池资源
Executors.newFixedThreadPool传入int参数来指定线程池中线程数量
11.3.2 Executors.newSingleThreadExecutor()
创建单例线程池,也就是线程池中只有一个线程
public class ExectorDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor();
try {
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
}
11.3.3 Executors.newSingleThreadExecutor()
创建可扩容的线程池,当有多个任务提交任务进入队列时,就会创建多个线程;当任务数量较少时,当前线程数量完全可以满足,就不进行扩容
public class ExectorDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
pool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
}
11.4 ThreadPoolExecutor底层原理
首先我们先看下通过工具类创建线程池的三种方式是如何进行创建的?
实际上三种方式都是通过创建ThreadPoolExecutor对象且已经指定了一些参数来创建线程池
11.4.1 7大参数
- corePoolSize:线程池中常驻线程核心数
- maximumPoolSize:线程池中能够容纳同时执行的最大线程数,必须大于1;
当阻塞队列满的时候,就会创建非核心线程
- keepAliveTime:多余的空闲线程的存活时间,当前线程池中线程数量超过核心线程数时(而非核心线程又没有使用),当空闲时间达到keepAlive时,多余线程就会被销毁直到只剩下核心线程数为止
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但是未执行的任务阻塞队列
- threadFactory:生成线程池中工作线程的线程工厂,用于创建线程;
一般使用默认即可
- handler:拒绝策略,当阻塞队列满了,且工作线程已经大于等于线程池的最大线程数时如何拒绝请求执行的runnable的策略
11.4.2 线程池处理流程
以银行作为例子进行解析:
1、银行当天的当值窗口也就是核心线程数,当有人办理业务(也就是提交任务)时就进入当值窗口
2、当当值窗口满了的时候,就开始排队等候(也就是阻塞队列)
3、当队列也满了的时候,没办法就只能把没有当值的窗口(最大线程数)也开了,再把队列中的人在非核心窗口办理,而之后到来的人进入队列
4、当队列再次满了的时候,就开始执行拒绝策略
5、办理的人越来越少,当非核心线程处于空闲状态,且已经超过了最大存活时间,就开始慢慢的关闭非核心线程
11.4.3 不能通过Executors创建线程池
阿里巴巴Java开发手册:
3. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资
源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者
“过度切换”的问题
4. 【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool :
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2) CachedThreadPool和ScheduledThreadPool :
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
11.5 ThreadPoolExecutor相关API
execute()
/**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*/
public void execute(Runnable command) {
}
传入一个Runnable接口对象,异步执行该任务,但是没有返回值也无法抛出异常
submit()
之前我们已经介绍过Executor
的继承体系,我们知道ThreadPoolExecutor
继承于AbstractExecutorService
类,那么就可以使用父类的相关方法
AbstractExecutorService#submit
方法:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
这里可以看出submit()方法既可以接收Runnable接口对象,也可以接收Callable接口对象
- 当接受Runnable接口对象时,返回值RunnableFuture类型对象中封装的泛型为void,故无法获取返回值
- 当接受Callable接口对象时,返回值RunnableFuture类型对象中封装的泛型为T,获取的实际上就是
RunnableFuture#call
方法的返回值 - 如果接收的是Callable接口对象,可以通过
submit()
返回的Future对象调用get()
方法获取返回值,当然get()
方法阻塞
invokeAll()
两种重载方式都是执行tasks集合中的所有任务,只是一个带有超时时间(这里的超时时间指的是所有任务执行完成的超时时间
),一个没有超时时间
测试
invokeAll()
不带超时时间
@Slf4j
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<Object>> futureList = pool.invokeAll(Arrays.asList(
() -> {
log.debug("begin");
TimeUnit.SECONDS.sleep(1);
return 1;
},
() -> {
log.debug("begin");
TimeUnit.SECONDS.sleep(2);
return 2;
},
() -> {
log.debug("begin");
TimeUnit.SECONDS.sleep(3);
return 3;
}
));
futureList.forEach(f -> {
try {
log.info("{}",f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}
测试结果
invokeAny()
11.6 任务调度线程池
11.6.1 Timer
在任务调度线程池功能加入之前,可以使用java.util.Timer
来实现定时功能
Timer的优点是简单易用,但由于所有的任务都是通过一个线程来调度,因此所有任务都是串行操作,同一时间只能有一个任务在执行,前一个任务的延迟或异常都会影响之后的任务
测试延迟对于Timer:
@Slf4j
public class Demo10 {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.info("task 1");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.info("task 2");
}
};
timer.schedule(task1,1000);
timer.schedule(task2,1000);
}
}
创建一个Timer
对象用于对任务进行调度
接着创建了两个TimerTask
作为任务将来被调度
然后调用Timer#schedule()
方法传入任务以及延迟执行的时间定时执行任务
测试结果
从时间间隔上可以看出task2在2s之后才执行,然而task2定时的时间间隔为1s,说明当前对定时任务的调度是同步的,明显影响性能
测试异常对于Timer的影响:
@Slf4j
public class Demo10 {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.info("task 1");
int i = 1 / 0;
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.info("task 2");
}
};
timer.schedule(task1,1000);
timer.schedule(task2,1000);
}
}
测试结果
可以看出task2并没有执行,因为task1抛出了异常且没有处理,也充分说明了Timer只有一个线程对任务进行处理
11.6.2 ScheduledExecutorService
测试ScheduledExecutorService任务并发执行:
@Slf4j
public class Demo10 {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.schedule(() -> {
log.info("task1");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,TimeUnit.SECONDS);
pool.schedule(() -> {
log.info("task1");
},1,TimeUnit.SECONDS);
}
}
这里使用ScheduledExecutorService#schedule()
来创建并执行定时任务
测试结果
从时间间隔上发现task2并没有等待task1执行完成才执行
ScheduledExecutorService#schedule()
ScheduledExecutorService
只是一个接口,而实际执行的是ScheduledThreadPoolExecutor
中的方法
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit) {
if (callable == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<V> t = decorateTask(callable,new ScheduledFutureTask<V(callable,triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
public ScheduledFuture<?> schedule(Runnable command,
long delay,TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(command,
new ScheduledFutureTask<Void>(command, null,
triggerTime(delay, unit)));
delayedExecute(t);
return t;
}
}
可以看到
schedule()
方法中可以传入Runnable接口对象,同样可以传入Callable接口对象,唯一的区别就是返回值ScheduledFuture
对象中是否泛型为void,如果为void,就不能从Future对象中获取返回值
long delay
参数表示延迟多少秒开始执行
测试异常对于ScheduledExecutorService任务的影响:
@Slf4j
public class Demo10 {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.schedule(() -> {
log.info("task1");
int i = 1 / 0;
},1,TimeUnit.SECONDS);
pool.schedule(() -> {
log.info("task1");
},1,TimeUnit.SECONDS);
}
}
测试结果
可以看出某个任务出现异常并不能妨碍其他任务的执行
但是这里并没有捕获到异常,也没有进行处理,且程序也没有正常退出,之后会对如何处理异常进行说明
11.6.2.1 ScheduledExecutorService循环定时任务
上面我们已经说过实际执行的类是ScheduledThreadPoolExecutor
,该类中的scheduleAtFixedRate()
方法就是用于循环定时任务,从名字中也可以看出是以固定的速率执行任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,null,
triggerTime(initialDelay, unit),unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
参数中的long initialDelay
表示延迟多久第一次执行任务,而long period
表示两次任务开始执行之间的时间间隔
测试
@Slf4j
public class Demo10 {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.scheduleAtFixedRate(() -> {
log.info("task1");
},1,1,TimeUnit.SECONDS);
}
}
测试结果
可以发现经过1s之后开始执行第一次任务,然后两次任务开始执行的时间间隔为1s
那么考虑一个问题,如果该任务内部会阻塞一段时间,且阻塞的时间超过了任务之间的间隔会发生什么呢
测试任务中阻塞一段时间
@Slf4j
public class Demo10 {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.scheduleAtFixedRate(() -> {
log.info("task1");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1,1,TimeUnit.SECONDS);
}
}
测试结果
可以发现任务循环执行的间隔为2s,说明当任务阻塞的时间已经超过了任务循环执行的间隔时间,那么任务循环执行的时间间隔就失效了
下面介绍ScheduledExecutorService的另一个循环执行定时任务的API:scheduleWithFixedDelay()
,从名字上就看出以确定的延迟执行任务
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,null,
triggerTime(initialDelay, unit),unit.toNanos(-delay));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
可以看出这里和上一个方法只有一个参数不同,那就是long delay
,实际上这个参数表示的是上一个任务执行完成之后,下一个任务需要延迟多少秒才执行(因此当任务阻塞时间过长时,该方法绝对不会延迟时间失效的问题)
核心不同点就是上一个方法判断的是上一个任务开始执行的间隔,而该方法判断的是上一个任务执行完成之后的间隔
测试第二种循环定时任务
这里可以发现每3s执行一次任务,实际上就是任务内部阻塞的时间加上任务之间延迟的时间
11.6.2.2 定时任务处理异常
第一种方式我们可以手动捕获异常:
@Slf4j
public class Demo10 {
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.scheduleWithFixedDelay(() -> {
try {
log.info("task1");
int i = 1 /0;
} catch (Exception e) {
e.printStackTrace();
}
},1,1,TimeUnit.SECONDS);
}
}
测试结果
这种方式对于普通的线程池一样有效,普通线程池也无法捕获异常,并不是指定定时任务线程池
我们知道对于线程池来说,可以通过execute()
方法或submit()
方法提交任务
两者之间的区别就是:
public void execute(Runnable command) {
schedule(command, 0, NANOSECONDS);
}
public <T> Future<T> submit(Callable<T> task) {
return schedule(task, 0, NANOSECONDS);
}
execute()
方法返回值为null,submit()
方法返回值为Future对象
之前我们已经说过,如果传入的参数为Callable接口对象,那么可以通过调用Future#get()
方法获取返回值
实际上当线程没有出现异常时获取的就是阻塞返回的返回值,当线程出现异常时由于无法获取返回值无法继续执行,故抛出异常
经过上面的分析,因此第二种方式我们通过传入Callable接口对象来获取异常信息
测试
public class Demo10 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Boolean> future = pool.submit(() -> {
log.info("task1");
int i = 1 / 0;
return true;
});
log.info("debug:{}",future.get());
}
}
测试结果
可以发现异常只是被抛出,并没有实际处理,且程序一直没有停止
11.6.2.3 定时任务实践
需求:每周四18:00执行任务
分析思路:
- 既然要每周四18:00执行任务,我们首先就要获取当前时间,然后判断当前时间和周四18:00的时间差
- 如果这时已经是周五,那么下周周四开始执行任务;如果这时只是周一,那么这周周四开始执行任务(
实际上这里就是获取第一次执行的延迟
) - 第一次执行之后就是每隔一周执行一次,因此我们要计算出一周的时间间隔
- 最后将定时任务逻辑写入定时任务线程池
测试
@Slf4j
public class TestScheduled {
public static void main(String[] args) {
// 计算每周的时间差
long period = TimeUnit.DAYS.toMillis(7);
// 计算当前时间
LocalDateTime now = LocalDateTime.now();
// 获取周四时间
LocalDateTime thursday = now.withHour(18).withMinute(0).withSecond(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
// 判断当前时间是否超过周四
if (now.compareTo(thursday) > 0) {
thursday = thursday.plusDays(7);
}
// 获取当前时间和周四间隔
long initDelay = Duration.between(now, thursday).toMillis();
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.scheduleAtFixedRate(() -> {
log.info("开始执行任务...");
},initDelay,period,TimeUnit.MILLISECONDS);
}
11.7 Tomcat线程池
我们知道在Tomcat中的一个Service中最核心的部分就是Connector和Container,而Connector负责接收请求,这里通信的IO框架实际上就是NIO,其通信模型如下:
- LimitLatch用来限流,可以控制最大连接个数,类似于JUC中的Semaphore
- Acceptor实际上就是一个线程,只负责接收新的Socket连接
- Poller负责监听Socket Channel是否有可读的IO事件(事件监听机制),这里可以是一个线程,也可以是一个线程组
- 一旦可读就封装一个任务对象(socketProcessor),提交给Executor线程池处理
- Executor线程池中的工作线程负责实际的业务逻辑(处理请求)