引言
线程是一种稀有的资源,java中的线程和操作系统的线程是1:1对应的,操作系统的线程是固定的,当我们有任务需要频繁调用线程执行的时候,可以用到线程池。合理利用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。
线程池的介绍
名称 | 作用 |
ExecutorService | 真正的线程池接口 |
ScheduledExecutorService | 类似于Timer/TimerTask,可以处理任务需要重复处理的问题 |
ThreadPoolExecutor | ExecutorService的默认实现 |
ScheduledThreadPoolExecutor | ScheduledExecutorService的实现 |
Executor是最顶层的接口,只有一个execute方法
void execute(Runnable command);
ExecutorService是对Executor的扩展,执行的任务可以是Callable类型,并且有了返回类型
Executors是一个工具类,可以通过这个工具类创建线程池
线程池的使用
CachedThreadPool
可缓存线程
corePoolSize = 0,maximumPoolSize设置为Integer.MAX_VALUE,代表没有核心线程,非核心线程是无界的;keepAliveTime = 60L,空闲线程等待新任务的最长时间是60s;用了阻塞队列SynchronousQueue,是一个不存储元素的阻塞队列,每一个插入操作必须等待另一个线程的移除操作,同理一个移除操作也得等待另一个线程的插入操作完成;
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
适用于处理大量耗时少的任务
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
Thread.sleep(600);
service.execute(new Thread(()->{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}));
}
System.out.println(service);
Thread.sleep(61000);
System.out.println(service);
}
如果线程池中有空闲的线程就会直接使用空闲的线程执行任务,线程空闲时间超过60s就会被销毁
FixedThreadPool
创建一个固定大小的线程池,每提交一个任务就创建一个线程,直到达到线程池的最大数量,后面提交的任务会进入等待队列,线程执行完任务就去等待队列中取任务执行,等待队列中的任务执行完后,线程销毁。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
service.execute(()->{
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
}
System.out.println(service);
Thread.sleep(1200);
System.out.println(service);
}
SingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作,如果这个唯一的线程因为异常介绍,那么会有一个新的线程代替它。单线程串行执行任务,此线程能保证任务执行的顺序是按照任务提交的顺序进行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
for(int i=0;i<5;i++){
service.execute(()->{
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
}
}
ScheduledThead
大小无限制的线程池,支持定时或者周期性的执行任务
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
service.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName());
},0,4,TimeUnit.SECONDS);
}
ThreadPoolExecutor
在使用ThreadPoolExecutor之前,需要说明的是,官方文档这样描述的:“这个类提供了许多可调节的参数和钩子函数,但还是推荐使用更为方便的工厂方法来创建线程池,也就是Executors工具类类创建”
* <p>To be useful across a wide range of contexts, this class
* provides many adjustable parameters and extensibility
* hooks. However, programmers are urged to use the more convenient
* {@link Executors} factory methods {@link
* Executors#newCachedThreadPool} (unbounded thread pool, with
* automatic thread reclamation), {@link Executors#newFixedThreadPool}
* (fixed size thread pool) and {@link
* Executors#newSingleThreadExecutor} (single background thread), that
* preconfigure settings for the most common usage
* scenarios. Otherwise, use the following guide when manually
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:核心线程数
maximumPoolSize:允许的最大线程数
当一个任务提交过来了,线程池中的线程数少于corePoolSize,这时候一个新的线程会被创建去处理这个任务,即使线程池中的线程有空闲状态的。当线程池中的线程数大于corePoolSize但是少于maximumPoolSize,只有当等待队列已经满了,才会去创建一个新的线程。将corepoolSize和maximumPoolSize设置成相等,就可以创建一个固定大小的线程池,将maximumPoolSize设置Integer.MAX_VALUE,就可以处理无穷多的任务,大多数情况下这两个数值是设置在构造方法里面的,但是也可以通过set方法动态改变。
keepAliveTime:存活时间
线程池中的线程数超过了corePoolSize,多余的线程空闲超过keepAliveTime就会被销毁
unit:时间单位
workQueue:等待队列
排队有三种策略:
直接提交。工作队列的默认选项是SynchronousQueue,它将任务直接提交给线程而不保持他们,如果此时没有线程可以立即执行这个任务,则试图把任务加入队列将失败。(其实就是将task放进队列的时候,如果这时候没有线程从队列中取task,就会一直处于阻塞状态,直到有线程空闲出来去队列中取任务,放和取是同步的)。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
BlockingQueue的选择:
SynchronousQueue:如Executor.newCachedThreadPool默认就是使用的SynchronousQueue
new ThreadPoolExecutor(2,
3,
30,
TimeUnit.SECONDS,
new SynchronousQueue,
Executors.DefaultThreadFactory,
new ThreadPoolExecutor.CallerRunsPolicy());
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
3,
30,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
new MyThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
for(int i=0;i<10;i++){
executor.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
}
}
当核心线程有两个正在运行
1.此时继续来了一个任务A,如果运行线程等于或者多于corePoolSize,则Executor首选将请求加入队列,而不添加新的线程。但是此时并没有线程要从队列中取任务,所以会新创建线程去执行。(不少文章对于这个场景的描述是不正确的,说是会添加到队列,既然是同步队列,没有线程来这个队列取任务,就没法保存任务)
2.继续再来一个任务B,会先尝试1中的描述,假设线程池中的任务都还是在运行状态,按逻辑是要取添加新线程,但是maximunPoolSize已经达到最大,只能将任务丢弃。为了不让这种情况出现,maximumPoolSize无界的就可以了。
jdk文档中说的很清:此策略可以避免在处理可能具有内部依赖性的请求集时出现锁
LinkedBlockingQueue:使用无界队列策略,如newFixedThreadPool默认使用的是LinkedBlockingQueue
使用无界队列,在所有的线程处于工作状态的时候会导致新增的任务被添加到这个队列。因此不会有线程数超过corePoolSize的情况,因此maximumPoolSize不会有任何效果。这种策略适合处理没有相互依赖的任务,例如web应用中突然爆发式的请求。
ArrayBlockingQueue:使用有界队列策略,这个是最为复杂的使用,最大的好处就是可以防止资源消耗过多。jdk不推荐使用也是有原因的。
RejectedExecutionHandler:当达到maximumPoolSize和队列的限制,再有任务过来就会到拒绝策略
CallerRunsPolicy:用回调线程去执行这个task
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2,
3,
30,
TimeUnit.SECONDS,
new SynchronousQueue<>(),
new MyThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
for(int i=0;i<10;i++){
executor.execute(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
});
}
}
还是执行之前的测试用例
回调线程其实就是main线程,本来main线程只把tasks提交到线程池,现在它还要去执行task,有了竞争那提交到线程池的速度就会降低。
AbortPolicy:处理任务遭到拒绝会抛出RejectedExcutionException异常,直接丢弃
DiscardPolicy:丢弃任务,不抛出异常
DiscardOldestPolicy:丢弃最老的没处理的任务,然后重试,如果这时候线程池关闭了,那任务就丢失了。
总结
keepAliveTime和maximumPoolSize及BlockingQueue的类型均有关系。如果BlockingQueue是无界的,那么永远不会触发maximumPoolSize,自然keepAliveTime也就没有了意义。
反之,如果核心数较小,有界BlockingQueue数值又较小,同时keepAliveTime又设的很小,如果任务频繁,那么系统就会频繁的申请回收线程。
ThreadFactory