线程池类关系图:
下面重点介绍下以下3个类:
类名 | 使用场景 |
ThreadPoolExecutor | 通用的线程池 |
ScheduledThreadPoolExecutor | 针对定时、周期性执行的任务的线程池 |
ForkJoinPool | 针对计算性任务,利用任务拆分、并行执行、工作窃取等机制,提升处理性能 |
ThreadPoolExecutor
构造函数
参数说明:
参数名 | 说明 | 详解 |
corePoolSize | 核心线程数 | 线程池保持的最少的线程数量。当有第一个任务时,才一次性创建corePoolSize个数的线程。 |
maximumPoolSize | 最大线程数 | 只有当队列满时,才会新建线程;如果当前的线程数量已经达到maximumPoolSize且workQueue已满,则走handler |
keepAliveTime | 空闲时间 | 当超过核心线程数量,且线程空闲时间超过keepAliveTime时,销毁该线程 |
unit | 时间单位 | keepAliveTime的时间单位 |
workQueue | 任务队列 | |
threadFactory | 线程工厂 | |
handler | 异常处理 |
workQueue说明
从队列添加/获取元素有几种不同的方法:
添加元素:
方法名 | 说明 |
boolean add(E e) | 非阻塞,插入成功返回true;如果队列满了则抛出IllegalStateExceptio |
boolwan offer(E e) | 非阻塞,插入成功返回true,如果队列满了则返回false |
void put(E e) | 阻塞,当队列满时会阻塞 |
获取元素:
方法名 | 说明 |
E take() | 阻塞,从队列头部获取并删除元素,如果队列为空,则阻塞直至有新元素出现 |
E poll(long timeout, TimeUnit unit) | 阻塞/非阻塞,从队列头部获取并删除元素,如果队列为空,则最多阻塞timeout |
SynchronousQueue
没有容量,直接提交队列,是无缓存等待队列,当任务提交进来,它总是马上将任务提交给线程去执行,如果线程已经达到最大,则执行拒绝策略;所以使用SynchronousQueue阻塞队列一般要求maximumPoolSize为无界(无限大),避免线程拒绝执行操作。从源码中可以看到容量为0:
//是否为空,直接返回的true
public boolean isEmpty() {
return true;
}
//队列大小为0
public int size() {
return 0;
}
LinkedBlockingQueue
默认情况下,LinkedBlockingQueue是个无界的任务队列,默认值是Integer.MAX_VALUE,当然我们也可以指定队列的大小。从构造LinkedBlockingQueue源码中可以看出它的大小指定方式:
//默认构造函数,大小为Integer最大
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
//也可以指定大小
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
为了避免队列过大造成机器负载,或者内存泄漏,我们在使用的时候建议手动传一个队列的大小。内部分别使用了takeLock和putLock对并发进行控制,添加和删除操作不是互斥操作,可以同时进行,这样大大提供了吞吐量。源码中有定义这两个锁:
//获取元素使用的锁
private final ReentrantLock takeLock = new ReentrantLock();
//加入元素使用的锁
private final ReentrantLock putLock = new ReentrantLock();
//获取元素时使用到takeLock锁
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
//加锁操作
takeLock.lock();
try {
//获取元素
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
//解锁
takeLock.unlock();
}
}
//添加元素到队列中使用putLock锁
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
//加锁操作
putLock.lock();
try {
//队列中存放的数据小于队列设置的值
if (count.get() < capacity) {
//添加元素
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
//解锁
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
ArrayBlockingQueue
可以理解为有界的队列,创建的时候必须要指定队列的大小,从源码可以看出构造的时候要传递值:
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
DelayQueue
是一个延迟队列,无界、队列中每个元素都有过期时间,当从队列获取元素时,只有过期的元素才会出队,而队列头部是最早过期的元素,若是没有过期,则进行等待。利用这个特性,我们可以用来处理定时任务调用的场景,例如订单过期未支付自动取消,设置一个在队列中过期的时间,过期了后,再去查询订单的状态,若是没支付,则调用取消订单的方法。
//获取元素
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
//获取元素
E first = q.peek();
if (first == null)
//进入等待
available.await();
else {
//获取过期时间
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
//小于等于0则过期,返回此元素
return q.poll();
first = null;
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//设置还需要等待的时间
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
RejectedExecutionHandler说明
AbortPolicy
直接抛出RejectedExecutionException
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
DiscardPolicy
直接丢弃任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
CallerRunsPolicy
立即执行任务
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
DiscardOldestPolicy
删除最老的任务(任务头部的任务),再把当前任务加进去
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
ScheduledThreadPoolExecutor
任务通过SeheduleFutureTask包装,存到DelayedWorkQueue保存,每次获取到时间的任务,并执行;如果是周期任务,再次放到DelayedWorkQueue
提交定时任务
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) | 非周期性任务,延时delay后执行,且没有返回参数 |
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { | 非周期性任务,延时delay后执行,有返回参数 |
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) | 周期性任务,延时delay后,按照period周期执行,没有返回参数 |
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit) | 和上面的很类似,区别: scheduleAtFixedRate:period指两次任务开始执行的间隔(如果任务执行时间大于period,则两次任务会同时执行) scheduleWithFixedDelay:period指一次任务执行完后,下一次任务的间隔。 |
ForkJoinPool
ForkJoinPool是一种特殊的线程池,继承AbstractExecutorService,适用于计算型的并行执行任务,主要是利用其“任务分治”、“任务窃取”特性;
任务分治
利用fork/join机制,将大任务,拆分成多个小任务,最终再合并小任务的结果:
任务用ForkJoinTask表示(当然ForkJoinPool也可以提交Runnable/Callable),有两个了类:
RecursiveTask:有返回值
RecursiveAction:没有返回值
我们写自己的任务时,一般继承上面的2个类,然后重写其compute方法,在compute方法里完成的任务的拆分、合并
示例:
下面以一个没有返回值的大任务为例,介绍一下RecursiveAction的用法
大任务是:打印0-100的数值。
小任务是:每次只能打印20个数值。
public class RaskDemo extends RecursiveAction {
/**
* 每个"小任务"最多只打印20个数
*/
private static final int MAX = 20;
private int start;
private int end;
public RaskDemo(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
//当end-start的值小于MAX时,开始打印
if((end-start) < MAX) {
for(int i= start; i<end;i++) {
System.out.println(Thread.currentThread().getName()+"i的值"+i);
}
}else {
// 将大任务分解成两个小任务
int middle = (start + end) / 2;
RaskDemo left = new RaskDemo(start, middle);
RaskDemo right = new RaskDemo(middle, end);
left.fork();
right.fork();
}
}
}
-------------------------------------------------------------------------------
public static void main(String[] args) throws Exception{
// 创建包含Runtime.getRuntime().availableProcessors()返回值作为个数的并行线程的ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 提交可分解的PrintTask任务
forkJoinPool.submit(new RaskDemo(0, 1000));
//阻塞当前线程直到 ForkJoinPool 中所有的任务都执行结束
forkJoinPool.awaitTermination(2, TimeUnit.SECONDS);
// 关闭线程池
forkJoinPool.shutdown();
}
运行结果:
从上面结果来看,ForkJoinPool启动了四个线程来执行这个打印任务,我的计算机的CPU是四核的。大家还可以看到程序虽然打印了0-999这一千个数字,但是并不是连续打印的,这是因为程序将这个打印任务进行了分解,分解后的任务会并行执行,所以不会按顺序打印。
任务窃取
下面有3个工作线程worker1、worker2、worker3,每个线程都有自己的队列(用双端队列Dqueue实现,实现LIFO-Last In First Out)。
当worker3的队列为空时,则从worker1、worker2的队列的头部窃取任务来执行,充分利用CPU
构造函数
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
parallelism | 并行度,相当于线程池中的线程数,默认Runtime.getRuntime().availableProcessors(),最大32767 |
ForkJoinWorkerThreadFactory | 线程创建工厂 |
UncaughtExceptionHandler | |
asyncMode | 是否异步模式,下面是源码: asyncMode ? FIFO_QUEUE : LIFO_QUEUE, FIFO即先进先出,即异步处理 LIFO即后进选出,即同步处理(有个问题:如果任务多,那先进的可能一直处理不了?) |
附件
参考:
https://blog.csdn.net/ZHANGLIZENG/article/details/127833510
https://blog.csdn.net/weixin_42039228/article/details/123206215