第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}
@Slf4j(topic = "c.BlockingQueue")
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(int capcity) {
this.capcity = capcity;
}
/**
* 阻塞获取:消费者线程
* 1、在从队列中获取任务时,需要加锁
* 2、如果队列空时,线程需要在等待队列上等待
* 3、获取队列头部的元素并返回
* 4、唤醒阻塞的生产者线程,告诉他队列不满了
*
*/
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();
}
}
/**
* 阻塞添加:生产者线程
* 1、向队列中添加任务时需要加锁
* 2、当队列满时,需要在等待队列上等待
* 3、向阻塞队列中添加任务
* 4、唤醒阻塞的消费者线程,告诉他队列不为空了
*
*/
public void put(T task) {
lock.lock();
try {
while (queue.size() == capcity) {
try {
log.debug("等待加入任务队列 {} ...", task);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
// 带超时阻塞获取
public T poll(long timeout, TimeUnit unit) {
lock.lock();
try {
// 将 timeout 统一转换为 纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
// 返回值是剩余时间
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();
}
}
// 带超时时间阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capcity) {
try {
if(nanos <= 0) {
return false;
}
log.debug("等待加入任务队列 {} ...", task);
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
// 判断队列是否满
if(queue.size() == capcity) {
rejectPolicy.reject(this, task);
} else { // 有空闲
log.debug("加入任务队列 {}", task);
queue.addLast(task);
emptyWaitSet.signal();
}
} finally {
lock.unlock();
}
}
}
@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务时的超时时间
private long timeout;
private TimeUnit timeUnit;
private RejectPolicy<Runnable> 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) {
/**
* 1、当任务数没有超过 coreSize 时,直接交给 worker 对象执行
* 2、如果任务数超过 coreSize 时,加入任务队列暂存
*/
synchronized (workers) {
if(workers.size() < coreSize) {
Worker worker = new Worker(task);
log.debug("新增 worker{}, {}", worker, task);
workers.add(worker);
worker.start();
} else {
//当队列满的时候,但是还有剩余的任务没有执行,那么有哪些选择:
// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行
// 4) 让调用者抛出异常
// 5) 让调用者自己执行任务
taskQueue.tryPut(rejectPolicy, task);
}
}
}
//worker代表一个线程
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run(){
/**
* 执行任务:
* 1、当 task 不为空,执行任务:task != null
* 2、当 task 执行完毕,从任务队列获取任务并执行:(task = taskQueue.take()) != null
*/
//while(task != null || (task = taskQueue.take()) != null) {
while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {
try {
log.debug("正在执行...{}", task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
synchronized (workers) {
log.debug("worker 被移除{}", this);
workers.remove(this);
}
}
}
}
@Slf4j(topic = "c.TestPool")
public class TestPool {
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, 1500, TimeUnit.MILLISECONDS);
//3、让调用者放弃任务执行:log.debug("放弃{}", task);
//4、让调用者抛出异常:throw new RuntimeException("任务执行失败 " + task);
//5、让调用者自己执行任务
task.run();
});
for (int i = 0; i < 4; i++) {
int j = i;
threadPool.execute(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", j);
});
}
}
}
可以通过ThreadPoolExecutor来创建一个线程池:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()
方法,线程池会提前创建并启动所有基本线程。
线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程
核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉
线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
该线程池中救急线程闲置超时时长
一个救急线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉
如果设置allowCoreThreadTimeOut = true,则会作用于核心线程
可选的单位有天、小时、分钟、毫秒、微秒和纳秒
用于保存等待执行的任务的阻塞队列
该线程池中的任务队列:维护着等待执行的Runnable对象
当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务
❑ ArrayBlockingQueue
:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则创建救急执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
❑ LinkedBlockingQueue
:一这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
❑ SynchronousQueue
:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
❑ PriorityBlockingQueue
:一个具有优先级的无限阻塞队列。
可以为线程创建时起个好名字
当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
❑ AbortPolicy
:直接抛出异常。
❑ CallerRunsPolicy
:让调用者运行任务
❑ DiscardOldestPolicy
:放弃队列中最早的任务,本任务取而代之
❑ DiscardPolicy
:放弃本次任务的执行
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
corePoolSize
并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue
排maximumPoolSize - corePoolSize
数目的线程来救急。(如果是无界队列,不会出现救急线程)maximumPoolSize
仍然有新任务这时会执行拒绝策略。corePoolSize
的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime
和 unit
来控制。Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类。
❑ 通过Executor框架的工具类Executors,可以创建3种类型的ThreadPoolExecutor。
❑ FixedThreadPool。
❑ SingleThreadExecutor。
❑ CachedThreadPool。
FixedThreadPool
被称为可重用固定线程数的线程池。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
FixedThreadPool
的corePoolSize
和maximumPoolSize
都被设置为创建FixedThreadPool
时指定的参数nThreads
。当线程池中的线程数大于corePoolSize
时,keepAliveTime
为救急线程等待新任务的最长时间,超过这个时间后救急线程将被终止。这里把keepAliveTime
设置为0L
,意味着救急线程会被立即终止。
如图为FixedThreadPool的execute()方法的运行示意图:
(1) 如果当前运行的线程数小于核心线程数coolPoolSize,那么就会创建新的线程来执行任务
(2) 如果当前运行的线程数等于核心线程数coolPoolSize,那么就会将任务加入LinkedBlockingQueue
(3) 线程执行完线程池中的任务后,会在循环中反复从LinkedBlockingQueue中取出任务来执行
FixedThreadPool
使用无界队列LinkedBlockingQueue
作为线程池的工作队列(队列的容量为Integer.MAX_VALUE
)。使用无界队列作为工作队列会对线程池带来如下影响。
maximumPoolSize
将是一个无效参数keepAliveTime
将是一个无效参数FixedThreadPool
不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution
方法)。特点:
面试题:单线程线程池的应用场景?
SingleThreadExecutor是使用单个worker线程的Executor。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
SingleThreadExecutor
的corePoolSize和maximumPoolSize
被设置为1。其他参数与FixedThreadPool
相同。SingleThreadExecutor
使用无界队列LinkedBlockingQueue
作为线程池的工作队列(队列的容量为Integer.MAX_VALUE
)。SingleThreadExecutor
使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool
相同
(1) 如果当前运行的线程数少于corePoolSize
(即线程池中无运行的线程),则创建一个新线程来执行任务。
(2) 如果当前线程池中有一个运行的线程,将任务加入LinkedBlockingQueue
。
(3) 线程执行完 (1) 中的任务后,会在一个无限循环中反复从LinkedBlockingQueue
获取任务来执行。
**使用场景:**希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
**区别:**自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一
个线程,保证池的正常工作
Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改,FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
面试题:如果提交一个cpu密集型的任务怎么选取线程池 ?
CachedThreadPool是一个会根据需要创建新线程的线程池。
CachedThreadPool
使用没有容量的SynchronousQueue
作为线程池的工作队列,但CachedThreadPool
的maximumPool
是无界的。这意味着,如果主线程提交任务的速度高于maximumPool
中线程处理任务的速度时,CachedThreadPool
会不断创建新线程。极端情况下,CachedThreadPool
会因为创建过多线程而耗尽CPU和内存资源。
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分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况。
(1) 首先执行SynchronousQueue.offer(Runnable task)
向同步队列中添加任务。如果当前maximumPool
中有救急线程正在执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS)
,那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成;否则执行(2)
(2) 当初始maximumPool
为空,或者maximumPool
中当前没有救急线程时,将没有线程执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS)
。这种情况下,步骤 (1) 将失败。此时CachedThreadPool
会创建一个新线程执行任务,execute()方法执行完成。
(3) 在步骤 (2) 中新创建的线程将任务执行完后,会执行SynchronousQueue. poll(keepAliveTime,TimeUnit.NANOSECONDS)
。这个poll操作会让空闲线程最多在SynchronousQueue
中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1)),那么这个救急线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。
SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行。
整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线
程也不会被释放。用来执行延迟或反复执行的任务
ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)放到一个DelayQueue中。
DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的Scheduled-FutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。
可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
threadsPool.execute(new Runnable() {
@Override
public void run() {
//TODO Auto-generated method stub
}
});
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
//处理中断异常
} catch (ExecutionException e) {
//处理无法执行任务异常
} finally {
//关闭线程池
executor.shutdown();
}
例如:submit方法:提交任务 task,用返回值 Future 获得任务执行结果
private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException {
Future<String> future = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("running");
Thread.sleep(1000);
return "ok";
}
});
log.debug("{}", future.get());
}