线程池实现原理分析
1.线程池的由来
很多小伙伴都会有这样的疑问,线程池是做什么的?线程池的工作原理?线程池可以解决什么问题呢?接下来我就为大家阐述一下我自己对于这些问题的见解。
- 线程池是做什么的:
在 Java 中,如果每个请求到达之后都创建一个线程的话,创建和销毁的过程是十分消耗系统资源的,甚至可能要比实际处理用户请求占用的时间和资源要多得多。
如果在一个jvm里创建过多的线程,可能会使系统由于过度消耗内存和“时间片切换过度”而导致系统资源不足。
为了解决这一问题才产生了线程池这个概念。
线程池做的就是复用已有资源(线程),控制资源总数。 - 线程池的工作原理:
线程池的核心逻辑就是提前将若干个线程放在一个容器中,如果有任务需要处理,则直接将任务分配给线程池中的线程处理,任务处理完成后该线程也不会被销毁,而是等待下一个任务的分配,这样就控制了线程的创建数量,避免了重复创建大量线程产生的巨大开销。 - 线程池可以解决什么问题:
上面两个问题已经将线程池的优势体现出来了
①合理使用线程池可以带来降低创建线程和销毁线程的性能开销;
②提高响应速度,有新任务时无需等待线程创建就可以马上执行;
③合理设置线程池大小可以避免因为线程数超过硬件资源瓶颈造成内存溢出等问题;
2.都有哪些常用线程池
由此看来线程池还是很有好处的,Java Jdk中也提供了几种线程池。 ExecutorService中有许多线程池的构建方法
ExecutorService service=Executors.newFixedThreadPool();//创建固定数量的线程池,线程数不变,当有一个任务提交
时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行.
ExecutorService service=Executors.newCacheThreadPool();//创建缓存线程池,最大线程数量为Integer.MAX_VALUE,可灵活回收线程,空闲的线程会在60秒后回收,无空闲线程再创建。
ExecutorService service=Executors.newSingleThreadExecutor();//创建一个单线程的线程池,若空闲则执行,没有空闲线程则挂着阻塞队列中。它只会用唯一的工作线程来执行任务, 保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
ExecutorService service=Executors.newScheduledThreadPool();//创建一个可以指定数量的线程池,带有延迟和周期性执行任务的功能,类似定时器。
各个线程池参数分析:
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数和最大线程数都是指定值,keepAliveTime为0。所以当线程池中的线程数超过核心线程数之后,其余线程会被放到阻塞队列中,超出最大线程数以外的空闲线程会被马上回收。
选用LinkeBlockingQueue阻塞队列,默认大小为Integer.MAX_VALUE,相当于没有上限。
FixedThreadPool执行任务的流程:
1.线程数小于核心线程数,新建线程执行任务。
2.线程数等于核心线程数之后,任务放入阻塞队列中。
3.由于队列容量很大,可以一直添加任务。
4.任务执行完成的线程反复去队列中取任务。
CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
CachedThreadPool是一个缓存线程池,如果线程超过处理需要,可灵活回收空闲线程,没有可回收线程则新建线程。并且该线程池没有核心线程数和非核心线程数上限,所以每个空闲线程存活时间只有60秒,超时就回收。
CachedThreadPool执行任务流程:
1.由于没有核心线程,直接向SynchronousQueue中提交任务
2.如果有空闲线程就取取任务执行,没有则创建新线程
3.执行完任务有60s存活时间,如果在60s内获取到新任务,则该线程可以继续存活下去,否则就会超时回收。
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
`
单线程线程池,只能用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO/LIFO)执行。
以上线程池都是基于ThreadPoolExecutor构建的,那么ThreadPoolExecutor都有哪些构造参数呢?
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize, //最大线程数
long keepAliveTime, //超时时间,超出核心线程数量以外的空闲线程存活时间
TimeUnit unit, //存活时间单位
BlockingQueue<Runnable> workQueue, //保存执行任务的队列
ThreadFactory threadFactory,//创建新线程使用的工厂
RejectedExecutionHandler handler //当任务无法执行的时候的处理方式)
线程池初始化时并没有创建线程,线程池里的线程初始化方式和其他线程初始化一样,但是在完成任务后不会自动销毁,而是以挂起的状态放回到线程池中。直到应用程序再次向线程池发出请求时,挂起的线程会再次执行任务。这样既节省了创建线程所造成的性能损耗,也可以让多个任务重复使用同一个线程,从而节约内存开销。
3.线程池的拒绝策略
1.AbortPolicy:直接抛出异常,默认策略;
2.CallerRunsPolicy:用调用者所在的线程来执行任务;
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
4.线程池的注意事项
阿里开发手册不建议使用线程池
不止一个同学问我说阿里开发手册上不建议使用线程池?估计这些同学都是没有认真看手册的。手册上是说线程池的构建不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。
我来简单分析下,用 Executors 使得用户不需要关心线程池的参数配置,意味着大家对于线程池的运行规则也会慢慢的忽略。这会导致一个问题,比如我们用 newFixdThreadPool 或者 singleThreadPool.允许的队列长度为Integer.MAX_VALUE,如果使用不当会导致大量请求堆积到队列中导致 OOM 的风险而 newCachedThreadPool,允许创建线程数量为Integer.MAX_VALUE,也可能会导致大量线程的创建出现 CPU 使用过高或者 OOM 的问题而如果我们通过ThreadPoolExecutor 来构造线程池的话,我们势必要了解线程池构造中每个参数的具体含义,使得开发者在配置参数的时能够更加谨慎。不至于像有些同学去面试的时候被问到:构造一个线程池需要哪些参数,都回答不上来。
如何合理配置线程池的大小
如何合理配置线程池大小,也是很多同学反馈给我的问题,我也简单说一下。线程池大小不
是靠猜,也不是说越多越好。
在遇到这类问题时,先冷静下来分析
- 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
一个公式:线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner测试大量运行次数求出平均值)
线程池中的线程初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实 际中如果需要 线程池创建之 后立即创建线 程,可以通过 以下两个方法 办到:prestartCoreThread():初始化一个核心线程; prestartAllCoreThreads():初始化所有核心线程
ThreadPoolExecutor tpe=(ThreadPoolExecutor)service;
tpe.prestartAllCoreThreads();
线程池的关闭
ThreadPoolExecutor 提 供 了 两 个 方 法 , 用 于 线 程 池 的 关 闭 , 分 别 是 shutdown() 和shutdownNow(),其中:shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务 shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务线程池容量的动态调整
ThreadPoolExecutor 提 供 了 动 态 调 整 线 程 池 容 量 大 小 的 方 法 : setCorePoolSize() 和setMaximumPoolSize(),setCorePoolSize:设置核心池大小 setMaximumPoolSize:设置线程池最大能创建的线程数目大小
任务缓存队列及排队策略
在前面我们多次提到了任务缓存队列,即 workQueue,它用来存放等待执行的任务。
workQueue 的类型为 BlockingQueue,通常可以取下面三种类型:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE;
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
线程池的监控
如果在项目中大规模的使用了线程池,那么必须要有一套监控体系,来指导当前线程池的状态,当出现问题的时候可以快速定位到问题。而线程池提供了相应的扩展方法,我们通过重写线程池的 beforeExecute、afterExecute 和 shutdown 等方式就可以实现对线程的监控,简单给大家演示一个案例
public class Demo1 extends ThreadPoolExecutor {
// 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间
private ConcurrentHashMap<String,Date> startTimes;
public Demo1(int corePoolSize, int maximumPoolSize, long
keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue);
this.startTimes=new ConcurrentHashMap<>();
}
@Override
public void shutdown() {
System.out.println("已经执行的任务数:
"+this.getCompletedTaskCount()+"," +
"当前活动线程数:"+this.getActiveCount()+",当前排队线程
数:"+this.getQueue().size());
System.out.println();
super.shutdown();
}
//任务开始之前记录任务开始时间
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTimes.put(String.valueOf(r.hashCode()),new Date());
super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
Date startDate = startTimes.remove(String.valueOf(r.hashCode()));
Date finishDate = new Date();
long diff = finishDate.getTime() - startDate.getTime();
// 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、
// 已完成任务数量、任务总数、队列里缓存的任务数量、
// 池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池
是否终止
System.out.print("任务耗时:"+diff+"\n");
System.out.print("初始线程数:"+this.getPoolSize()+"\n");
System.out.print("核心线程数:"+this.getCorePoolSize()+"\n");
System.out.print("正在执行的任务数量:"+this.getActiveCount()+"\n");
System.out.print("已经执行的任务
数:"+this.getCompletedTaskCount()+"\n");
System.out.print("任务总数:"+this.getTaskCount()+"\n");
System.out.print("最大允许的线程数:"+this.getMaximumPoolSize()+"\n");
System.out.print("线程空闲时
间:"+this.getKeepAliveTime(TimeUnit.MILLISECONDS)+"\n");
System.out.println();
super.afterExecute(r, t);
}
public static ExecutorService newCachedThreadPool() {
return new Demo1(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new
SynchronousQueue ());
}
}
测试脚本:
public class Test implements Runnable{
private static ExecutorService es =Demo1.newCachedThreadPool();
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
es.execute(new Test());
}
es.shutdown();
}
}
Callable/Future 使用及原理分析
线程池还有两种执行任务方法:submit()/execute;这两个方法区别在哪里呢?
- 1.execute只可以接收一个Runnable参数
2.execute需要抛异常
3.execute没有返回值
4.submit可以接收Callabel和Runnable两种类型的参数
5.对于submit方法,如果传入Callable参数会得到一个Future类型的返回值
6.submit调用不需要抛异常,除非调用了Future.get()
Callable/Future 和 Thread 之类的线程构建最大的区别在于,能够很方便的获取线程执行完
以后的结果。首先来看一个简单的例子
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
return "hello world";
}
public static void main(String[] args) throws ExecutionException,
InterruptedException {
CallableDemo callableDemo=new CallableDemo();
FutureTask futureTask=new FutureTask(callableDemo);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
为什么需要使用回调呢?因为结果值是另一个线程计算的,当前的线程是不知道的结果值什么时候计算完成的,所以它传递一个回调接口给计算线程,当计算完成时,调用回调接口,回传结果值。
这个在很多地方有用到,比如 Dubbo 的异步调用,比如消息中间件的异步通信等等…
利用 FutureTask、Callable、Thread 对耗时任务(如查询数据库)做预处理,在需要计算结果之前就启动计算。
所以我们来看一下 Future/Callable 是如何实现的。
Callable/Future 原理分析
在刚刚实现的 demo 中,我们用到了两个 api,分别是 Callable 和 FutureTask。
Callable 是一个函数式接口,里面就只有一个 call 方法。子类可以重写这个方法,并且这个方法会有一个返回值。
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
FutureTask实现了RunnableFuture接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
RunnableFuture接口,继承了Runnable、Futrue这两个接口,Runnable接口是实现线程的接口,Future接口表示一个任务的周期,并提供了相应方法判断任务是否完成或取消,以及获取任务结果等。
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
// 当前的 Future 是否被取消,返回 true 表示已取消
boolean isCancelled();
// 当前 Future 是否已结束。包括运行完成、抛出异常以及取消,都表示当前 Future 已结束
boolean isDone();
// 获取 Future 的结果值。如果当前 Future 还没有结束,那么当前线程就等待,
// 直到 Future 运行结束,那么会唤醒等待结果值的线程的。
V get() throws InterruptedException, ExecutionException;
// 获取 Future 的结果值。与 get()相比较多了允许设置超时时间
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
FutureTask
FutureTask 是 Runnable 和 Future 的结合,如果我们把 Runnable 比作是生产者,Future 比作是消费者,那么 FutureTask 是被这两者共享的,生产者运行 run 方法计算结果,消费者通过 get 方法获取结果。
作为生产者消费者模式,有一个很重要的机制,就是如果生产者数据还没准备的时候,消费者会被阻塞。当生产者数据准备好了以后会唤醒消费者继续执行。
这个就和阻塞队列有些相似,那么FutureTask是如何实现的呢?
state(很重要):表示FutureTask的当前状态,一共七种状态
private static final int NEW = 0; // NEW 新建状态,表示这个 FutureTask还没有开始运行
// COMPLETING 完成状态, 表示 FutureTask 任务已经计算完毕了
// 但是还有一些后续操作,例如唤醒等待线程操作,还没有完成。
private static final int COMPLETING = 1;
// FutureTask 任务完结,正常完成,没有发生异常
private static final int NORMAL = 2;
// FutureTask 任务完结,因为发生异常。
private static final int EXCEPTIONAL = 3;
// FutureTask 任务完结,因为取消任务
private static final int CANCELLED = 4;
// FutureTask 任务完结,也是取消任务,不过发起了中断运行任务线程的中断请求
private static final int INTERRUPTING = 5;
// FutureTask 任务完结,也是取消任务,已经完成了中断运行任务线程的中断请求
private static final int INTERRUPTED = 6;
run方法
其实 run 方法作用非常简单,就是调用 callable 的 call 方法返回结果值 result,根据是否发生异常,调用 set(result)或setException(ex)方法表示 FutureTask 任务完结。不过因为 FutureTask 任务都是在多线程环境中使用,所以要注意并发冲突问题。注意在 run方法中,我们没有使用 synchronized 代码块或者 Lock 来解决并发问题,而是使用了 CAS 这个乐观锁来实现并发安全,保证只有一个线程能运行 FutureTask 任务。
源码如下:
public void run() {
// 如果状态 state 不是 NEW,或者设置 runner 值失败
// 表示有别的线程在此之前调用 run 方法,并成功设置了 runner 值
// 保证了只有一个线程可以运行 try 代码块中的代码。
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {/ 只有 c 不为 null 且状态 state 为 NEW 的情况
V result;
boolean ran;
try {
result = c.call(); //调用 callable 的 call 方法,并获得返回结果
ran = true;//运行成功
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex); //设置异常结果,
}
if (ran)
set(result);//设置结果
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
get方法
get 方法就是阻塞获取线程执行结果,这里主要做了两个事情
- 判断当前的状态,如果状态小于等于 COMPLETING,表示 FutureTask 任务还没有完结,所以调用 awaitDone 方法,让当前线程等待。
- report 返回结果值或者抛出异常
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
awaitDone
如果当前的结果没有执行完,则将当前线程插入到等待队列。被阻塞的线程,会等到 run 方法执行结束之后被唤醒。
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false; // 节点是否已添加
for (;;) {
// 如果当前线程中断标志位是 true,
// 那么从列表中移除节点 q,并抛出 InterruptedException 异常
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
int s = state;
if (s > COMPLETING) { // 当状态大于 COMPLETING 时,表 示 FutureTask 任务已结束。
if (q != null)
q.thread = null; // 将节点 q 线程设置为 null,因为线程没有阻塞等待
return s;
}// 表示还有一些后序操作没有完成,那么当前线程让出执行权
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
//表示状态是 NEW,那么就需要将当前线程阻塞等待。
// 就是将它插入等待线程链表中,
else if (q == null)
q = new WaitNode();
else if (!queued)
// 使用 CAS 函数将新节点添加到链表中,如果添加失败,那么 queued 为 false,
// 下次循环时,会继续添加,知道成功。
queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
else if (timed) {// timed 为 true 表示需要设置超时
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos); // 让当前线 程等待 nanos 时间
}
else
LockSupport.park(this);
}
}
report
report 方法就是根据传入的状态值 s,来决定是抛出异常,还是返回结果值。这个两种情况都表示 FutureTask 完结了
private V report(int s) throws ExecutionException {
Object x = outcome;//表示 call 的返回值
if (s == NORMAL) // 表示正常完结状态,所以返回结果值
return (V)x;
// 大于或等于 CANCELLED,都表示手动取消 FutureTask 任务
// 所以抛出 CancellationException 异常
if (s >= CANCELLED)
throw new CancellationException();
// 否则就是运行过程中,发生了异常,这里就抛出这个异常
throw new ExecutionException((Throwable)x);
}
线程池对于 Future/Callable 的执行
我们现在再来看线程池里面的 submit 方法,就会很清楚了
public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
return "hello world";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService es=Executors.newFixedThreadPool(1);
CallableDemo callableDemo=new CallableDemo();
Future future=es.submit(callableDemo);
System.out.println(future.get());
}
}
AbstractExecutorService.submit
调用抽象类中的 submit 方法,这里其实相对于 execute 方法来说,只多做了一步操作,就是
封装了一个 RunnableFuture
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
ThreadpoolExecutor.execute
然后调用 execute 方法,这里面的逻辑前面分析过了,会通过 worker 线程来调用过 ftask 的
run 方法。而这个 ftask 其实就是 FutureTask 里面最终实现的逻辑。