Java中的线程池
一、 自定义线程池
1. Blocking Queue
线程池中需要定义一个阻塞队列,用于存放生产者生产的,需要消费者执行的任务。
public class BlockingQueue<T>{
//1.任务队列:存放任务对象
private Deque<T> queue = new ArrayDeque<>();
//2.锁:锁住任务队列的头部和尾部,避免队列头部一个任务被多个消费者抢走,同时,避免多个生产者向队列尾部存放任务时产生线程安全问题
private ReentrantLock lock = new ReentrantLock();
//3.生产者条件变量:当队列中没有任务时,消费者需要阻塞等待
private Condition fullWaitSet = lock.newCondition();
//4.消费者条件变量:队列中任务达到容量上限后,生产者需要阻塞等待
private Condition emptyWaitSet = lock.newCondition();
//5.容量:定义队列中可以存放的任务容量
private int capcity;
public BlockingQueue(){
}
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
//1.阻塞获取
public T take(){
lock.lock();
try{
while(queue.isEmpty()){
try{
emptyWaitSet.await();
}catch(InterruptedException e){
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally{
lock.unlock();
}
}
//2.阻塞添加
public void put(T task){
lock.lock();
try{
while(queue.size() == capcity){
try{
fullWaitSet.await();
}catch(InterruptedException e){
e.printStackTrace();
}
}
queue.addLast(task);
emptyWaitSet.signal();
}finally{
lock.unlock();
}
}
//3.获取大小
public int size(){
lock.lock();
try{
return queue.size();
}finally{
lock.unlock();
}
}
//4.带超时的阻塞获取
public T poll(long timeout, TimeUnit unit){
lock.lock();
try{
//将超时时间统一转换为ns
long nanos = unit.toNanos(timeout);
while(queue.isEmpty()){
try{
//如果该线程自然醒来,却没有获取到任务,则返回null
if(nanos <= 0){
return null;
}
//返回的是剩余的时间,解决了虚假唤醒问题
//如果某个被唤醒的线程未达到睡眠时间就被唤醒,且还没有获取到任务,则继续沉睡
nanos = emptyWaitSet.awaitNanos(nanos);
}catch(InterruptedException e){
e.printStackTrace();
}
}
//线程被唤醒后获得了任务,或者自然醒来后获得任务
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
}finally{
lock.unlock();
}
}
//5.带超时的阻塞添加
public boolean offer(T task, long timeout, TimeUnit unit){
lock.lock();
try{
//将超时时间统一转换为ns
long nanos = unit.toNanos(timeout);
while(queue.size() == capcity){
try{
if(nanos <= 0){
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
}catch(InterruptedException e){
e.printStackTrace();
}
}
queue.addLast(task);
emptyWaitSet.signal();
return true;
}finally{
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
//判断队列是否已满
if(queue.size() == capcity){
rejectPolicy.reject(this, task);
}else{//队列没满
queue.addLast(task);
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
}
2. ThreadPool
public class ThreadPool {
//任务队列
private BlockingQueue<Runnable> taskQueue;
//线程集合
private HashSet<Worker> workers = new HashSet<>();
//核心线程数
private int coreSize;
//获取任务的超时时间:当线程没有任务时可以存活的最长时间
private long timeout;
//时间单位
private TimeUnit timeUnit;
//拒绝策略
private RejectPolicy rejectPolicy;
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity, RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}
//执行任务
public void execute(Runnable task){
//当任务数没有超过coreSize时,直接将任务交给worker执行,否则将任务加入任务队列暂存起来
synchronized (workers){
if(workers.size() < coreSize){
Worker worker = new Worker();
workers.add(worker);
worker.start();
}else{
//尝试将任务放入阻塞队列
taskQueue.tryPut(rejectPolicy, task);
}
}
}
//用于包装线程Thread的类
class Worker extends Thread{
private Runnable task;
public Worker(){
}
public Worker(Runnable task){
this.task = task;
}
@Override
public void run() {
//执行任务
//① 当task不为空,执行任务
//② 当task执行完毕,从任务队列中获取新的任务并执行
// while(task != null || (task = taskQueue.take()) != null){
while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null){
try{
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
task = null;
}
}
//当没有任务之后,从任务队列中移除本线程
synchronized (workers){
workers.remove(this);
}
}
}
}
3. 拒绝策略
@FunctionalInterface
public interface RejectPolicy<T> {
//拒绝策略其实有以下几种
//1.任务队列满了,则让调用者死等
//2.任务队列满了,则让调用者等待一段时间,在该时间内,若队列有空位则添加,超过该时间还未添加则放弃
//3.任务队列满了,则让调用者直接放弃
//4.任务队列满了,让调用者抛出异常
//5.任务队列满了,让调用者自己执行
void reject(BlockingQueue<T> queue, T task);
}
4. 测试
public class TestThreadPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1, (queue, task) -> {
//1.任务队列满了,则让调用者死等
//queue.put(task);
//2.任务队列满了,则让调用者等待一段时间,在该时间内,若队列有空位则添加,超过该时间还未添加则放弃
//queue.offer(task);
//3.任务队列满了,则让调用者直接放弃
//啥也不做
//4.任务队列满了,让调用者抛出异常
throw new RuntimeException("任务执行失败" + task);
//5.任务队列满了,让调用者自己执行
//task.run();
});
for(int i = 0; i < 4; i++){
int temp = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
}catch (InterruptedException e){
e.printStackTrace();
}
});
}
}
}
二、JDK中的线程池
1. 线程池状态
ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量。这样的话,就可以将线程池状态与线程个数合二为一,所有的信息可以存储在一个原子变量ctl中,用一次CAS原子操作进行两个值的同时更改。
1.更改源码
// 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; }
2.状态描述
状态名 | 高3位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
---|---|---|---|---|
RUNNING | 111 | Y | Y | - |
SHUTDOWN | 000 | N | Y | 不会接收新任务,但会处理阻塞队列剩余任务 |
STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列任务 |
TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 |
TERMINATED | 011 | - | - | 终结状态 |
注意:从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING(RUNNING是负数,其余的是正数)。
2. 构造方法
2.1 详解构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数目(最多保留的线程数),核心线程在完成任务后,不会被销毁。
- maximumPoolSize:最大线程数目,线程池中最多存放多少线程,是核心线程和救急线程的和。
- keepAliveTime:生存时间,针对救急线程,救急线程在完成任务后,会等待一定的生存时间,该时间内没有执行任务,则被销毁。
- unit:时间单位,针对救急线程。
- workQueue:阻塞队列,当前的核心线程都已经在执行任务时,如果还有任务到来,则被存放在阻塞队列中。
- threadFactory:线程工厂,可以在线程创建时为该线程起个好名字。
- handler:拒绝策略,当核心线程都在运行任务,阻塞队列已满,救急线程也都在执行任务,则线程池无法再接受任务会采用拒绝策略,决定如何对待这些任务。
2.2 工作流程
- 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
- 当线程数达到corePoolSize时,并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue队列排队,直到有空闲的线程。
- 如果队列选择了有界队列,那么任务超过了队列大小时,会创建maximumPoolSize - corePoolSize数目的救急线程来救急,救急线程会处理新加入的任务,而不是阻塞队列中先到的任务。
- 如果线程到达maximumPoolSize仍然有新任务这时会执行拒绝策略。JDK中提供了四种拒绝策略。
- 当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime和unit来控制。
2.3JDK中的四种拒绝策略
- AbortPolicy让调用者抛出RejectedExecutionException异常,这是默认策略。
- CallerRunsPolicy让调用者运行任务。
- DiscardOldestPolicy放弃队列中最早的任务,本任务取而代之。
- DiscardPolicy放弃本次任务。
3. 线程池的分类
3.1 固定大小的线程池newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
该线程池的核心线程数就是最大线程数,不会创建应急线程,因此也就无需超时时间。它的阻塞队列是无界的,可以存放任意数量的任务。该线程适用于任务量已知,相对耗时的任务。
3.2 带缓冲线程池newCachedThreadPool
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
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
该线程的。线程数固定为1,任务数多于1时,会放入无界队列排队,从而使所有的任务串行执行。该线程和自己创建一个线程的区别是,如果自己创建的线程任务执行失败而终止,那么没有任何补救措施;但是该线程池还会新建一个线程,保证池的正常工作。该线程和newFixedThreadPool(1)的区别是,Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,它对外暴露的是ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改;但是Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改,因为FinalizableDelegatedExecutorService应用的是装饰器模式,所以该线程池只对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法,也就不能修改。
3.4 任务调度线程池ScheduledExecutorService
在任务调度线程池功能加入之前,可以使用java.util.Timer来实现定时功能,Timer的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。ScheduledExecutorService则解决了Timer所存在的问题。
方法 | 作用 |
---|---|
schedule(函数式接口, delay, TimeUnit) | 在指定的delay事件后开始执行任务,TimeUnit规定了时间单位 |
scheduleAtFixedRate(函数式接口, initialDelay, period, TimeUnit) | 在指定延时后以一定的速率执行任务,在开始initialDelay后,每隔period时间执行一次任务,TimeUnit规定了时间单位,如果任务执行时间大于period则任务接连执行 |
scheduleWithFixedRate(函数式接口, initialDelay, delay, TimeUnit) | 在指定延时后以一定的速率执行任务,在开始initialDelay后,每次任务执行结束后,间隔delay时间后,执行一次任务,TimeUnit规定了时间单位 |
4. 线程池的方法
4.1 执行任务
方法 | 作用 |
---|---|
void execute(Runnable command) | 执行任务 |
Future<T> submit(Callable<T> task) | 提交任务,使用返回值Future获得任务执行结果 |
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) | 提交tasks中的所有任务 |
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) | 提交tasks中的所有任务,如果当前提交的这些任务在指定时间内没有执行完,则后续任务无法执行 |
T invokeAny(Collection<? extends Callable<T>> tasks) | 提交tasks中的所有任务,任意有一个任务执行完毕,则返回此任务执行结果,其他任务取消 |
T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) | 带超时时间的invokeAny方法 |
4.2 关闭线程池
方法 | 作用 |
---|---|
void shutdown() | 将线程池状态变为SHUTDOWN,该方法不会阻塞调用线程的执行,调用线程执行完shutdown()方法会继续执行之后的代码,该线程池内的任务会慢慢执行完毕 |
List<Runnable> shutdownNow() | 将线程池状态变为STOP |
4.3 其他方法
方法 | 作用 |
---|---|
boolean isShutdown() | 不在RUNNING状态的线程池,此方法就返回true |
boolean isTerminated() | 线程池状态是否是TERMINATED |
boolean awaitTermination(long timeout, TimeUnit unit) throw InterruptedException | 调用shutdown后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事情,可以利用此方法等待一定的时间,超过时间线程池还任务还没全部运行结束,则不再等待,继续运行 |
5. 线程池内的异常处理
如果在线程池内,不处理异常,则异常不会被发现,所以,需要我们人为对异常进行处理:
① 人为利用try-catch进行处理。
② 利用返回值Future接收,会将异常信息封装在Future对象中,此时如果调用Future对象,则会报出异常。
6. Tomcat线程池
- LimitLatch用来限流,可以控制最大连接个数,类似JUC中的Semaphore。
- Acceptor只负责接收新的socket连接。
- Poller只负责监听socket channel是否有可读的I/O事件。
- 一旦可读,封装一个任务对象(socketProcessor),提交给Executor线程池处理。
- Executor线程池中的工作线程最终负责处理请求。
- Tomcat线程池扩展了ThreadPoolExecutor,行为稍有不同。如果总线程数达到 maximumPoolSize,这时不会立刻抛出RejectedExecutionException异常,而是再次尝试将任务放入队列,如果还失败,才抛出RejectedExecutionException异常。
7. 高级线程池-Fork/Join
7.1 基本概念
- Fork/Join是JDK1.7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算。所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解。
- Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率。
- Fork/Join 默认会创建与 cpu 核心数大小相同的线程池。
7.2 基本使用
提交给Fork/Join线程池的任务需要继承RecursiveTask(有返回值)或RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务.
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask1(5)));
}
class AddTask1 extends RecursiveTask<Integer> {
int n;
public AddTask1(int n) {
this.n = n;
}
@Override
protected Integer compute() {
// 如果 n 已经为 1,可以求得结果了
if (n == 1) {
return n;
}
// 将任务进行拆分(fork)
AddTask1 t1 = new AddTask1(n - 1);
t1.fork();
// 合并(join)结果
int result = n + t1.join();
return result;
}
}
7.3 优化使用
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask3(1, 10)));
}
class AddTask3 extends RecursiveTask<Integer> {
int begin;
int end;
public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
// 5, 5
if (begin == end) {
return begin;
}
// 4, 5
if (end - begin == 1) {
return end + begin;
}
// 1 5
int mid = (end + begin) / 2; // 3
AddTask3 t1 = new AddTask3(begin, mid); // 1,3
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5
t2.fork();
int result = t1.join() + t2.join();
return result;
}
}