1、为什么使用线程池
池化技术的思想主要是为了减少每次获取资源的消耗,提高资源的利用率;线程池、数据库连接池等都是这个设计思路。
(1)降低资源的消耗
;通过重复利用已经创建的线程降低线程新建和销毁时产生的消耗。
(2)提高响应速度
;当任务到达时,不需要等线程创建就可以直接执行任务。
(3)提高线程的可管理性
;线程是稀缺资源,如果无限的创建线程,不仅消耗系统资源,还会影响系统的稳定性,使用线程池可以同一个调优、分配和监控线程。
2、Executor框架
Java1.5
之后通过Executor
框架来实现线程池。通过Executor
来启动线程比Thread
的start
方法更好,除了效率高、便于管理之外,还可以防止this逃逸
。
this逃逸是指在构造函数返回之前,其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能产生奇怪的错误
Executoe
框架不仅提供了线程管理,还提供了线程工厂、队列和拒绝策略等,让并发编程变的更加简单。
2.1、Executor框架结构(三个部分)
(1)任务(Runnable/Callable
)
执行的任务需要实现Runnable
和Callable
接口。
(2)任务的执行(Executor
)
执行任务的核心是Executor
接口和继承Executor
接口的ExecutorService
接口。
(3)异步计算的结果(Future
)
Future
接口以及该接口的实现类FutureTask
类都可以代表异步计算的结果。
当我们把Runnable
和Callable
接口的实现类交给ThreadPoolExecutor
执行时,调用submit
方法可以返回FutureTask
对象。
2.2、Executor框架的使用示意图
(1)主线程首先创建实现Runnable
或Callable
接口的任务对象。
(2)将创建完成的对象给ExecutorService
执行。
(3)如果执行ExecutorService.submit()
方法,则返回一个FutureTask
对象。
(4)最后,主线程可以执行FutureTask.get()
方法等待任务线程完成,获取执行FutureTask.cancel()
方法来取消任务线程。
3、Executor框架核心类ThreadPoolExecutor
ThreadPoolExecutor
类是Executor
框架的核心类。
ThreadPoolExecutor
的构造方法分析,参数最全的一个,其他构造方法调用这个方法,某些参数给与默认值。
public ThreadPoolExecutor(int corePoolSize, //线程池核心线程数量
int maximumPoolSize, //线程池最大线程数量
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程最大存活时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> 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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor
最重要的三个参数:
(1)corePoolSize
:核心线程数量,就是最小同时可以执行的线程数量。
(2)maximumPoolSize
:当队列中存放的任务达到队列容量时,当前可以同时执行的线程数量变为最大线程数。
(3)workQueue
:当新任务来的时候会判断当前执行的线程数量是否达到核心线程数量,达到的话,新任务存放在队列中。
ThreadPoolExecutor
其他参数:
(1)keepAliveTime
:当线程池的线程数量大于corePoolSize
时,核心线程外的其他线程不会立即销毁,而是会等待,知道等待时间超过keepAliveTime
就会被销毁。
(2)unit
:keepAliveTime
时间单位。
(3)threadFactory
:创建新线程时会用到。
(4)handler
:拒绝策略也叫饱和策略。
ThreadPoolExecutor
饱和策略:当同时执行的线程任务达到最大线程数量并且任务队列也满了的时候,可以定义一些策略来处理新来的任务。
(1)ThreadPoolExecutor.AbortPolicy
:抛出 RejectedExecutionException
来拒绝新任务的处理。
(2)ThreadPoolExecutor.CallerRunsPolicy
:调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
(3)ThreadPoolExecutor.DiscardPolicy
:不处理新任务,直接丢弃掉。
(4)ThreadPoolExecutor.DiscardOldestPolicy
:此策略将丢弃最早的未处理的任务请求。
4、阿里巴巴推荐使用ThreadPoolExecutor来创建线程池
(1)通过构造方法创建
(2)通过Executor
框架的工具类Executors
来创建三种类型的ThreadPoolExecutor
阿里巴巴强制不允许使用Executors
工具类来创建默认的线程池,因为通过ThreadPoolExecutor
创建线程池可以让开发者能明确线程池运行规则,避免资源耗尽。
Executors
返回的线程池弊端如下
(1)FixedThreadPool
和SingleThreadExecutor
:允许请求的队列长度为Integer.MAX_VALUE
,会造成请求堆积,导致OOM
(内存溢出)
(2)CachedThreadPool
和ScheduledThreadPool
:允许创建的线程数量为Integer.MAX_VALUE
,可能会创建大量线程,导致OOM
。
5、ThreadPoolExecutor使用示例
5.1、实现Runnable接口的任务类
package com.kk.first.thread;
import java.util.Date;
public class TestRunnable implements Runnable{
private String name;
public TestRunnable(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(name + ":start -time:" + new Date());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + ":end -time:" + new Date());
}
}
5.2、创建ThreadPoolExecutor
线程池
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
poolTest();
}
public static void poolTest() {
// 核心线程5,最大线程10,空闲线程最大等待销毁时间1L,任务队列容量100,CallerRunsPolicy饱和策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 1L,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
TestRunnable thread = new TestRunnable("线程" + i);
executor.execute(thread);
}
executor.shutdown();//终止线程池
while (!executor.isTerminated()) {
}
}
}
结果如下
线程1:start -time:Fri Mar 12 10:59:02 CST 2021
线程2:start -time:Fri Mar 12 10:59:02 CST 2021
线程0:start -time:Fri Mar 12 10:59:02 CST 2021
线程3:start -time:Fri Mar 12 10:59:02 CST 2021
线程4:start -time:Fri Mar 12 10:59:02 CST 2021
线程1:end -time:Fri Mar 12 10:59:07 CST 2021
线程0:end -time:Fri Mar 12 10:59:07 CST 2021
线程5:start -time:Fri Mar 12 10:59:07 CST 2021
线程3:end -time:Fri Mar 12 10:59:07 CST 2021
线程4:end -time:Fri Mar 12 10:59:07 CST 2021
线程7:start -time:Fri Mar 12 10:59:07 CST 2021
线程8:start -time:Fri Mar 12 10:59:07 CST 2021
线程2:end -time:Fri Mar 12 10:59:07 CST 2021
线程6:start -time:Fri Mar 12 10:59:07 CST 2021
线程9:start -time:Fri Mar 12 10:59:07 CST 2021
线程5:end -time:Fri Mar 12 10:59:12 CST 2021
线程8:end -time:Fri Mar 12 10:59:12 CST 2021
线程7:end -time:Fri Mar 12 10:59:12 CST 2021
线程6:end -time:Fri Mar 12 10:59:12 CST 2021
线程9:end -time:Fri Mar 12 10:59:12 CST 2021
5.3、分析上述示例的线程池原理
从输出结果可以看出,线程会优先执行5个任务,5个任务中有任务执行完则有新任务补充上
。
分析下线程池执行线程的execute方法
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//任务队列
private final BlockingQueue<Runnable> workQueue;
public void execute(Runnable command) {
// 如果任务为null,则抛出异常。
if (command == null)
throw new NullPointerException();
// ctl 中保存的线程池当前的一些状态信息
int c = ctl.get();
// 下面会涉及到 3 步 操作
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;
// 然后,启动该线程从而执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,
// 并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
if (!isRunning(recheck) && remove(command))
reject(command);
// 如果当前线程池为空就新创建一个线程并执行。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;
//然后,启动该线程从而执行任务。
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
else if (!addWorker(command, false))
reject(command);
}
addWorker()
方法主要是用来创建新的工作线程,创建成功则返回true。
上面的逻辑如下图所示
ThreadPoolExecutor
线程池的源码分析可以看看这个文章JUC线程池ThreadPoolExecutor源码分析
5.4、ThreadPoolExecutor
的相关方法
(1)execute()
:用于提交不需要返回值的任务,所以无法判断线程是否执行成功。
(2)submit()
:用于提交需要返回值的任务,返回一个Futrue
类型的对象,通过Futrue
类型的对象可以判断线程是否执行成功。
(3)shutdown()
:关闭线程池,线程池不在接收新任务,但是任务队列中线程需要执行完。
(4)shutdownnow()
:关闭线程池,线程池会中止当前正在执行的任务,并停止处理并返回队列中排队的任务。
(5)isShutDown()
: 当调用shutdown()
方法后返回为true
。
(6)isTerminated()
: 当调用shutdown()
方法后,并且所有提交的任务完成后返回为 true
。
6、线程池大小的确定
线程池的大小设置的太大和太小都有问题,合适的最好。
如果设置的线程池大小太小的话,同一时间有大量的任务/请求要处理,会导致大量的任务/请求在任务队列中排队等待执行,甚至任务队列满了后,无法处理新的任务/请求。同时,大量的任务堆积在任务队列也会导致OOM
。没有充分的利用CPU
资源。
如果设置的太大的话,大量的线程可能会同时争取cpu
资源,导致大量的上下文切换,从而增减的线程的执行时间,影响了整体效率。
有一个简单并且适用面比较广的公式:
(1)CPU 密集型任务(N+1)
: 这种任务消耗的主要是CPU
资源,可以将线程数设置为 N(CPU 核心数)+1
,比 CPU
核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU
的空闲时间。
(2)I/O 密集型任务(2N)
: 这种任务应用起来,系统会用大部分的时间来处理 I/O
交互,而线程在处理I/O
的时间段内不会占用 CPU
来处理,这时就可以将 CPU
交出给其它线程使用。因此在I/O
密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N
。
如何判断是CPU
密集任务还是IO
密集任务?
CPU
密集型简单理解就是利用CPU
计算能力的任务,比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO
密集型,这类任务的特点是 CPU
计算耗费时间相比于等待IO
操作完成的时间来说很少,大部分时间都花在了等待IO
操作完成上。
当然,上述通过公式计算的数值都是理论上合适的,但是具体落地的业务场景复杂,并没有一个行之有效的公式能计算出业务场景下合适的线程池大小。美团在这块探索了一个别致的办法,既然我不能确定线程池大小,那我能不能降低修改线程池配置参数的成本,当遇到问题时,能快速即时的修改线程池参数?于是美团整出个动态化配置线程池核心参数的操作
ThreadPoolExecutor
提拱了可以修改线程池配置的方法,但是少了一个核心参数任务队列,看源码发现
原来时队列的capacity被final修饰了,于是美团把LinkedBlockingQueue复制下改了个名字,去掉capacity变量的final修饰。搞定~~
具体美团的骚操作可以看看他们的这篇文章Java线程池实现原理及其在美团业务中的实践