目录
一、为什么需要线程池:
多线程的异步执行方式,虽然能够最大限度发挥多核计算机的计算能力,但是如果不加控制,反而会对系统造成负担。线程本身也要占用内存空间,大量的线程会占用内存资源并且可能会导致Out of Memory。即便没有这样的情况,大量的线程回收也会给GC带来很大的压力。
为了避免重复的创建线程,线程池的出现可以让线程进行复用。通俗点讲,当有工作来,就会向线程池拿一个线程,当工作完成后,并不是直接关闭线程,而是将这个线程归还给线程池供其他任务使用。
二、线程池的简介:
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果线程数超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
线程池的主要特点为:线程复用、控制最大并发数、管理线程。
三、线程池的优势:
为何要用线程池 即 线程池的优势:
总体来说,线程池有如下的优势:
(1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
四、线程池的架构图:
interface:接口
两个接口:
1. 第一个接口是Executor,它只定义了一个方法execute(Runnable command),相当于定义了一个框架,它能够执行一个任务。Executor就像是定义了一个框架,并确定了这个框架能够提供的功能。
2. 第二个接口是ExecutorService,它继承Executor。主要扩展了关闭线程管理的一些功能,比如shutdown方法用来关闭线程池的任务,isTerminated方法来判断线程池的任务是否结束。
public interface ExecutorService extends Executor //继承了顶级接口Executor
另外ExecutorService还提供了最重要的方法submit,它支持了有返回结果的任务提交,也支持没返回值的任务提交,也引入了实现这个功能最关键的接口Callable、Future;
ExecutorService支持的比较广泛:
submit(Callable task):支持参数Callable,Callable是有返回值的接口
submit(Runnable task):支持参数Runnable ,Runnable是没有返回值的接口
Executor、ExecutorService都是接口(接口只定义方法,不实现方法),只是定义了提交任务、关闭任务等方法,相当于只是申明了线程池支持这些功能。
①:execute(Runnable command):执行行Ruannable类型的任务
②:submit(task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
③:shutdown():温柔的关闭线程池,停止接受新任务,并执行完未完成的任务。
④:shutdownNow():强制关闭线程池,未完成的任务会以列表形式返回!
⑤:isTerminated():返回所有任务是否执行完毕。当调用shutdown()方法后,并且所有提交的任务完成后返回为true;当调用shutdownNow()方法后,成功停止后返回为true;
⑥:isShutdown():返回线程池是否关闭,当调用shutdown()或shutdownNow()方法后返回为true。
3. AbstractExecutorService抽象类:
public abstract class AbstractExecutorService implements ExecutorService
AbstractExecutorService:它是抽象类并继承ExecutorService接口。它主要是实现了ExecutorService接口的submit系列方法。
实现submit方法主要依靠RunnableFuture接口和它的实现类FutureTask。RunnableFuture继承Runnable和Future接口,而它的实现FutureTask的一个属性callable是Callable类型。
FutureTask实现了Runnable的run方法,run方法调用的是callable的call方法保存执行结果。同时也实现Future的get方法获取结果,如果任务还没有执行则阻塞线程。
所以submit方法实现的主要过程是提交的任务Callable封装成FutureTask,并把FutureTask当作Runnable丢给execute方法去异步执行,然后把FutureTask当作Future作为submit返回值。
但是AbstractExecutorService并没有实现execute方法,所以它是一个抽象类,在等待有缘人来实现execute,实现线程池的最后一步。
4. 实现类ThreadPoolExecutor:
ThreadPoolExecutor是Executor框架的正真实现者。它实现了execute方法,execute真正的实现,以下三个关键点:
HashSet workers;//Worker集合,线程池
BlockingQueue workQueue;//阻塞队列,要执行的任务
final void runWorker(Worker w);//Worker调用这个方法,可以从阻塞队列中获取任务来执行。
线程池工具类:Executors:
类似于集合和集合工具类
数组和数据的工具类
线程和线程的工具类
Executors中有多个用于创建线程池的方法;
五、Executors工具类中创建线程池的方法基本使用:
1.代码演示包含固定数量线程的线程池:
package com.fan.threadpool;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo1 {
public static void main(String[] args) {
//使用线程的工具类Executors来创建线程池,固定数量的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
try {
for (int i = 1; i <= 10; i++) {
//execute方法用于执行任务
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();//关闭线程池
}
}
}
2.代码演示包含一个线程的线程池:
package com.fan.threadpool;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo1 {
public static void main(String[] args) {
//使用线程的工具类Executors来创建线程池,固定数量的线程池
//一池包含5固定数量的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//一池包含一个线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
try {
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
for (int i = 1; i <= 10; i++) {
//execute方法用于执行任务
/*threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 办理业务");
});*/
threadPool2.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
/*threadPool.shutdown();*/ //关闭线程池
threadPool2.shutdown();//关闭线程池2
}
}
}
3.代码演示包含N个线程 的线程池:
package com.fan.threadpool;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo1 {
public static void main(String[] args) {
//使用线程的工具类Executors来创建线程池,固定数量的线程池
//一池5固定数量的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//一池包含一个线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
//一池包含N个线程,N的数量根据任务量大小动态变化
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
for (int i = 1; i <= 10; i++) {
//execute方法用于执行任务
/*threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 办理业务");
});*/
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
/*threadPool.shutdown();*/ //关闭线程池
threadPool3.shutdown();//关闭线程池
}
}
}
或者这样:
package com.fan.threadpool;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolDemo1 {
public static void main(String[] args) {
//使用线程的工具类Executors来创建线程池,固定数量的线程池
//一池5固定数量的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//一池包含一个线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
//一池包含N个线程,N的数量根据任务量大小动态变化
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
for (int i = 1; i <= 10; i++) {
//execute方法用于执行任务
/*threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 办理业务");
});*/
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 办理业务");
});
//MILLISECONDS:/ˈmɪlisekənd/毫秒
try { TimeUnit.MILLISECONDS.sleep(200);}
catch (InterruptedException e) {e.printStackTrace();}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
/*threadPool.shutdown();*/ //关闭线程池
threadPool3.shutdown();//关闭线程池
}
}
}
创建线程池的方法解读:
本质上都是调用了ThreadPollExecutor(7个参数)方法:
六、线程池的7个重要参数:
从源码中可以看出,线程池的构造函数有7个参数,分别是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。下面会对这7个参数一一解释。
一、corePoolSize 线程池核心线程大小:
核心线程数,也即最小的线程数/常驻线程数,线程池中会维护一个最小的线程数量,即使这些线程处于空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
二、maximumPoolSize 线程池最大线程数量:
一个任务被提交到线程池以后,首先会找有没有 空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中。
三、keepAliveTime 空闲线程存活时间:(多余/空闲线程数 = 最大线程数 - 核心线程数)
多余/空闲线程数 也可以叫非核心线程,核心线程+非核心线程=最大线程数
当线程池中的现存线程数量大于corePoolSize的时 候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被销毁,最终会收缩到corePoolSize的大小。
四、unit 空闲线程存活时间单位:
keepAliveTime的计量单位
五、workQueue 工作阻塞队列:
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
①:ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
②:LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
③:SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQuene;
④:priorityBlockingQuene:具有优先级的无界阻塞队列;
六、threadFactory线程工厂。它是ThreadFactory类型的变量,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。
七、handler拒绝策略。线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
-
AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行(生产上一般不用,因为报异常)。
-
CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
-
DiscardPolicy - 丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式。
-
DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
线程池监控API:
getActiveCount() :获取线程池中正在执行任务的线程数量
getCompletedTaskCount():获取已执行完毕的任务数
getTaskCount() :获取线程池已执行与未执行的任务总数
getPoolSize():获取线程池当前的线程数
getQueue().size():获取队列中的任务数
七、自定义一个线程池(自定义7大参数):
package com.fan.threadpool;
import java.util.concurrent.*;
public class MyThreadPoolDemo2 {
public static void main(String[] args) {
//自定义一个线程池
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5),//阻塞队列的容量,等候区
Executors.defaultThreadFactory(),//使用工具类创建默认的线程工厂
new ThreadPoolExecutor.AbortPolicy()//饱和拒绝策略
);
try {
for (int i = 1; i <=7 ; i++) {
final int temp = i;
//execute相当于原先的run方法
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+
"\t 号窗口,服务顾客"+temp);
try { TimeUnit.SECONDS.sleep(3);}
catch (InterruptedException e) {e.printStackTrace();}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();//关闭线程池
}
}
}
当把for循环次数(即任务数)小于等于(核心线程数+阻塞队列容量)时,只创建核心线程处理任务:
当把for循环次数(即任务数)大于(核心线程数+阻塞队列容量)时,开始创建非核心线程处理任务:
当 任务数 大于 (线程规定的最大容量+阻塞队列容量) ,则触发饱和拒绝策略:
画图模拟线程池的处理过程:
核心线程:2
阻塞队列容量:3
最大线程数:5
过程:
八、线程池工作原理:
线程池工作院里的额底层:
-
在创建了线程池后,等待提交过来的任务请求。
-
当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
- 如果 正在运行的线程数量 小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量 大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
- 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。
九、4种拒绝策略(handler):
当 任务数 大于 (线程规定的最大容量+阻塞队列容量) ,则触发饱和拒绝策略:
二、拒绝策略定义:
拒绝策略提供顶级接口 RejectedExecutionHandler ,其中方法 rejectedExecution 即定制具体的拒绝策略的执行逻辑。
jdk默认提供了四种拒绝策略:
-
AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行(生产上一般不用,因为报异常,阻止了系统的正常运行)。
-
CallerRunsPolicy - “调用者运行策略”是一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
-
DiscardPolicy - 直接丢弃任务,但是不抛出异常。如果允许任务丢失,这是最好的一种方案;可以配合这种模式进行自定义的处理方式。
-
DiscardOldestPolicy - 丢弃队列中等待最久的任务,当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
阿里开发手册规定不能使用 工具类Executors中的方式创建线程池:
代码演示四种拒绝策略的发生:
第一种AbortPolicy:
第二种CallerRunsPolicy:
main线程是调用者,多余任务被调用者main线程部分或者全部执行了:
第三种DiscardPolicy:直接丢弃:
第四种DiscardOldestPolicy:丢弃最老的:
总结:
四种拒绝策略是相互独立无关的,选择何种策略去执行,还得结合具体的业务场景。实际工作中,一般直接使用 ExecutorService 的时候,都是使用的默认的 defaultHandler ,也即 AbortPolicy 策略。
拒绝策略的选用:
第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
十、合理设置线程池最大容量maximumPoolSize:
获得cpu核数:
System.out.println(Runtime.getRuntime().availableProcessors());
线程池核心线程数多少最为合适:分两种情况讨论:
CPU 密集型:
CPU密集型也叫计算密集型,cpu使用率较高(也就是一些复杂运算,逻辑处理)
比如说要计算1+2+3+…+ 1亿、计算圆周率后几十位、数据分析,视频高清解码等都是属于CPU密集型程序。
特点:占用cpu资源,线程切换 无开销
多核CPU处理CPU密集型程序才合适,而且中间可能没有线程的上下文切换(一个核心处理一个线程)
IO密集型:
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,但CPU的使用率不高。
所以用脚本语言像python去做I/O密集型操作,效率就很快。
简单的说,就是需要大量的输入输出,比如读文件、写文件、传输文件、网络请求。比如接收一个前端请求–解析参数–查询数据库–返回给前端这样的,那么就是IO密集型的,例如web应用
区别和使用:
IO密集型:大量网络,文件操作
CPU 密集型:大量计算,cpu 占用越接近 100%, 耗费 多个核或多台机器
业务要具体分析,假如CPU现在是10%,数据量增大一点点,CPU狂飙,那也可能CPU密集型。
IO密集型的设置线程数的大小有三种种参考:
方法一:
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数2
2cpu核数:这种情况是假设cpu使用率达到了100%
方法二:
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数 /(1 - 阻系数)
比如8核CPU:8/(1 - 0.9)=80个线程数
阻塞系数在0.8~0.9之间;
方式三:
在《Java并发编程实践》中,是这样来计算线程池的线程数目的:
一个基准负载下,使用 几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = W/C:wait 等待时间/calculate 计算时间 比率
为保持处理器达到期望的使用率,最优的池的大小等于:
Nthreads = Ncpu x Ucpu x (1 + W/C)
即线程池数量=cpu核数 x cpu使用率 x (w/c+1)
有一个估算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
W/C:wait 等待时间/calculate 计算时间
这个公式进一步转化为:生产中主要用这种方式
最佳线程数目 = (线程等待时间w与线程CPU时间之比 + 1)* CPU数目
以上可以得出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
是否使用线程池就一定比使用单线程高效呢?
答案是否定的,比如Redis就是单线程的,但它却非常高效,基本操作都能达到十万量级/s。从线程这个角度来看,部分原因在于:
- 多线程带来线程上下文切换开销,单线程就没有这种开销
- 锁