线程的创建和销毁,都涉及到系统调用,比较消耗系统资源,所以就引入了线程池技术,避免频繁的线程创建和销毁。
合理地使用线程池能够带来的好处:
1) 通过重复利用已创建的线程降低资源消耗;
2) 提高任务的响应速度;
3) 使用线程池可以对线程进行统一分配、调优和监控。
线程池是一组线程的集合,当提交一个新任务到线程池时,处理流程大致如下:
1) 线程池判断核心线程池里的线程是否都在执行任务,如果不是,则创建一个新的工作线程来执行任务。
2) 如果核心线程池里的线程都在执行任务,判断工作队列是否已经满,如果没有,则将任务存储在工作任务中。
3) 如果工作队列满了,判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果满了,则交给饱和策略来处理这个任务。
Java的线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供,框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。
1) Executor接口是线程池框架中最基础的部分,定义了一个用于执行Runnable的execute方法。
public interface Executor {
void execute(Runnable command);
}
2) ExecutorService接口定义了线程池的具体行为:
void shutdown();
List shutdownNow();
boolean isShutdown();
boolean isTerminated();
Future submit(Callable task);
Future submit(Runnable task, T result);
Future> submit(Runnable task);
3) AbstractExecutorService子类:实现ExecutorService接口,为各类执行器类提供基础。
4)ScheduledExecutorService接口:添加了处理延迟执行或者周期任务。
5)ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
6) ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执
行命令。ScheduledThreadPoolExecutor比Timer更灵活,功能更强大。
框架使用示意图:
Executor框架最核心的类是ThreadPoolExecutor,是线程池的主要实现类,被称为可重用固定线程数的线程池。主要由下列4个组件构成。
3.2.1 核心数据结构
a. 状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS; // 接受新任务, 且处理已经进入阻塞队列的任务
private static final int SHUTDOWN = 0 << COUNT_BITS; // 不接受新任务, 但处理已经进入阻塞队列的任务
private static final int STOP = 1 << COUNT_BITS; // 不接受新任务, 且不处理已经进入阻塞队列的任务, 同时中断正在运行的任务
private static final int TIDYING = 2 << COUNT_BITS; // 所有任务都已终止, 工作线程数为0, 线程转化为TIDYING状态并准备调用terminated方法
private static final int TERMINATED = 3 << COUNT_BITS; //terminated方法已经执行完成
b. Worker内部类
每一个Worker代表一个线程。Worker继承于AQS,也就是说Worker本身就是一把锁,用
于线程池的关闭、线程执行任务的过程中。
public class ThreadPoolExecutor extends AbstractExecutorService {
//...
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 存放任务的阻塞队列
private final BlockingQueue workQueue;
// 对线程池内部各种变量进行互斥访问控制
private final ReentrantLock mainLock = new ReentrantLock();
// 线程集合
private final HashSet workers = new HashSet();
//...
private final class Worker extends AbstractQueuedSynchronizer implements
Runnable {
// ...
final Thread thread; // Worker封装的线程
Runnable firstTask; // Worker接收到的第1个任务
volatile long completedTasks; // Worker执行完毕的任务个数
// ...
}
}
3.2.2 核心配置参数解析
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue 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;
}
3.2.3 任务执行过程
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 如果当前线程数小于corePoolSize,则启动新线程
if (workerCountOf(c) < corePoolSize) {
// 添加Worker,并将command设置为Worker线程的第一个任务开始执行
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果当前的线程数大于或等于corePoolSize,则调用workQueue.offer放入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 如果线程池正在停止,则将command任务从队列移除,并拒绝command任务请求。
if (! isRunning(recheck) && remove(command))
reject(command);
// 放入队列中后发现没有线程执行任务,开启新线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 线程数大于maxPoolSize,并且队列已满,调用拒绝策略
else if (!addWorker(command, false))
reject(command);
}
execute方法执行情况:
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤
需要获取全局锁)。
2) 如果运行的线程数>=corePoolSize,则将任务加入BlockingQueue;
3)如果队列已满,则创建新任务来处理任务(注意,执行这一步骤需要获取全局锁);
4) 如果当前运行的线程超过了maximumPoolSize,任务将拒接,并调用RejectedExecutionHandler.rejectedExecution()方法。
流程图如下:
3.2.4 线程池的优雅关闭
线程池有两个关闭方法,shutdown()和shutdownNow(),这两个方法会让线程池切换到不同的状
态。在队列为空,线程池也为空之后,进入TIDYING 状态;最后执行一个钩子方法terminated(),进入TERMINATED状态,线程池才真正关闭。
在调用 shutdown()或者shutdownNow()之后,线程池并不会立即关闭,接下来需要调用 awaitTermination() 来等待线程池关闭。关闭线程池的正确步骤:
// executor.shutdownNow();
executor.shutdown();
try {
boolean flag = true;
do {
flag = ! executor.awaitTermination(500, TimeUnit.MILLISECONDS);
} while (flag);
} catch (InterruptedException e) {
// ...
}
shutdown()与shutdownNow()的区别:
1) shutdown()不会清空任务队列,会等所有任务执行完成,shutdownNow()清空任务队列。
2) shutdown()只会中断空闲的线程,shutdownNow()会中断所有线程。
3.2.5 拒绝策略
在execute(Runnable command)方法中,调用了reject(command)执行拒绝策略。
触发条件:
1)核心线程池满,阻塞队列满,非核心线程池满;
2)ThreadPoolExecutor关闭。
private volatile RejectedExecutionHandler handler;
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
RejectedExecutionHandler 是一个接口,定义了四种实现,分别对应四种不同的拒绝策略:
AbortPolicy::丢弃任务并抛出RejectedExecutionException(默认使用);
CallerRunsPolicy:调用者直接在自己的线程里执行,线程池不处理;
DiscardOldestPolicy:阻塞队列丢弃最近任务,执行当前任务;
DiscardPolicy:空运转,什么都不干。
线程池核心数设置:
1.先看下机器的CPU核数,然后在设定具体参数:
System.out.println(Runtime.getRuntime().availableProcessors());
即CPU核数 = Runtime.getRuntime().availableProcessors()
2.分析下线程池处理的程序是CPU密集型,还是IO密集型
CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:核心线程数 = CPU核数 * 2
注:IO密集型(某大厂实践经验)
核心线程数 = CPU核数 / (1-阻塞系数) 例如阻塞系数 0.8,CPU核数为4
则核心线程数为20
通过Executor框架的工具类Executors,可以创建各种不同类型的ThreadPoolExecutor。
a. newSingleThreadExecutor 单线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
b. newFixedThreadPool 固定线程数的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
c. newCachedThreadPool 可缓存的线程池
每接收一个请求,就创建一个线程来执行。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
d. newScheduledThreadPool 周期调度的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
e. newWorkStealingPool (JDK1.8新增) 具有抢占式操作的线程池
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
阻塞队列BlockingQueue继承了Queue 接口,是一个支持阻塞的插入和移除的队列。
阻塞队列的典型使用场景就是 生产者/消费者模式。生产者是向队列添加元素的线程;消费者是从队列取元素的线程。
在阻塞队列不可用时,队列操作有以下四种处理方式:
方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | —— | —— |
ArrayBlockingQueue 是最典型的有界队列,其内部是用数组存储元素的,利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程。
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
参数fair表示是否指定公平,默认不保证线程公平的访问线程,即则塞的线程在队列可用时都可以争夺访问队列的资格,可能存在插队。反之公平则是按照阻塞的先后顺序访问队列,为了保证公平性,通常会降低吞吐量。
LinkedBlockingQueue是一个用链表实现的无界阻塞队列。队列的默认最大长度为Integer.MAX_VALUE。同样利用 ReentrantLock 实现线程安全,使用 Condition 来阻塞和唤醒线程。无法设置ReentrantLock 的公平非公平,默认是非公平。
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node(null);
}
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序
升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则。或者初始化
时,指定构造参数Comparator来对元素进行排序。
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
public PriorityBlockingQueue(int initialCapacity,
Comparator super E> comparator) {
if (initialCapacity < 1)
throw new IllegalArgumentException();
this.comparator = comparator;
this.queue = new Object[Math.max(1, initialCapacity)];
}
DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素的时候,可以指定多久才能从队列获取当前元素。只有在延迟期满时才能从队列中提取元素。
队列获取数据,first为空表示队列为空或者等待时间没有耗尽直接返回空
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
return (first == null || first.getDelay(NANOSECONDS) > 0)
? null
: q.poll();
} finally {
lock.unlock();
}
}
SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue 的容量不是 1 而是 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。SynchronousQueue的吞吐量高于
LinkedBlockingQueue和ArrayBlockingQueue。
public SynchronousQueue() {
this(false);
}
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue() : new TransferStack();
}
public E peek() {
return null;
}
LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法。
LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回。我们称这种节点操作为“匹配”方式。
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。相比其他的阻塞队列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First单词结尾的方法,表示插入、获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入、获取或移除双端队列的最后一个元素。