线程池
Java线程池是运用最多的并发框架,线程池的优点如下:
1.降低资源消耗:通过重复利用已创建的线程降低线程的创建和销毁带来的消耗。
2.提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
3.提高线程的可管理性:由线程池统一管理调度进行线程分配和监控。
一、为什么要用线程池?
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,就是将资源统一在一起管理的一种思想。
线程池的主要思想是:在进程开始时创建一定数量的线程,并加到池中以等待工作。当服务器收到请求时,它会唤醒池内的一个线程(如果有可用线程),并将需要服务的请求传递给它。一旦线程完成了服务,它会返回到池中再等待工作。如果池内没有可用线程,那么服务器会等待,直到有空线程为止。
线程池具有以下优点: - 用现有线程服务请求比等待创建一个线程更快。
- 线程池限制了任何时候可用线程的数量。这对那些不能支持大量并发线程的系统非常重要。
- 将要执行任务从创建任务的机制中分离出来,允许我们采用不同策略运行任务。例如,任务可以被安排在某一个时间延迟后执行,或定期执行。
二、线程池的核心参数
Java中的线程池核心实现类是ThreadPoolExecutor,ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ThreadPoolExecutor,主要构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize:核心线程大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使有其他空闲线程可以处理任务也会创新线程,等到工作的线程数大于核心线程数时就不会在创建了。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前把核心线程都创造好,并启动
-
maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且以创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果我们使用了无界队列,那么所有的任务会加入队列,这个参数就没有什么效果了
-
keepAliveTime:线程池的工作线程空闲后,保持存活的时间。如果没有任务处理了,有些线程会空闲,空闲的时间超过了这个值,会被回收掉。如果任务很多,并且每个任务的执行时间比较短,避免线程重复创建和回收,可以调大这个时间,提高线程的利用率。
-
unit:keepAliveTIme的时间单位,可以选择的单位有天、小时、分钟、毫秒、微妙、千分之一毫秒和纳秒。类型是一个枚举java.util.concurrent.TimeUnit,这个枚举也经常使用,有兴趣的可以看一下其源码
-
workQueue:工作队列,用于缓存待处理任务的阻塞队列,常见的有4种,后面有介绍
-
threadFactory:线程池中创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
-
handler:饱和策略,当线程池无法处理新来的任务了,那么需要提供一种策略处理提交的新任务,默认有4种策略
example:
public class Demo1 {
static ThreadPoolExecutor executor = new ThreadPoolExecutor(3,
5,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int j = i;
String taskName = “任务” + j;
executor.execute(() -> {
//模拟任务内部处理耗时
try {
TimeUnit.SECONDS.sleep(j);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + taskName + “处理完毕”);
});
}
//关闭线程池
executor.shutdown();
}
}
三、任务调度流程
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
四、线程池中常见5种工作队列
任务太多的时候,工作队列用于暂时缓存待处理的任务,JDK中常见的5种阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按照先进先出原则对元素进行排序
- LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按照先进先出排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool使用了这个队列。
- SynchronousQueue :一个不存储元素的阻塞队列,每个插入操作必须等到另外一个线程调用移除操作,否则插入操作一直处理阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用这个队列
- PriorityBlockingQueue:一个具备优先级的无限阻塞队列,进入队列的元素按照优先级会进行排序
- DelayQueue:没有大小限制的队列,只有当其指定的延迟时间到了,才可以从队列中获取到该元素,所有往队列中插入数据的操做(生产者)永远不会被阻塞,而只有获取数据的操做(消费者)才会被阻塞。常见的例子是使用一个DelayQueue来管理一个超时未响应的链接队列。
五、四种常见饱和策略
AbortPolicy:直接抛出异常
CallerRunsPolicy:在当前调用者的线程中运行任务,即随丢来的任务,由他自己去处理
DiscardOldestPolicy:丢弃队列中最老的一个任务,即丢弃队列头部的一个任务,然后执行当前传入的任务
DiscardPolicy:不处理,直接丢弃掉,方法内部为空
六、Executors类
Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。常用的方法有
-
newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。 -
newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待。 -
newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒处于等待任务到来)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。 -
newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池的处理流程:
1.首先判断核心线程池里的线程是否都在执行任务。若不是,则创建一个新的工作线程来执行任务,如果核心线程都在执行任务,转到下一个流程。
2.线程池判断工作队列是否已满,如果队列没有满,则交给工作队列处理,若已满则进入下一个流程。
3.线程池判断线程池的线程是否都已处于工作状态,若没有,则创建下一个工作线程执行任务,如果已满,则交给饱和策略处理任务。
线程池的使用:
线程池构造方法:
public ThreadPoolExecutor(int corePoolSize,//线程池的基本大小
int maximumPoolSize,//线程池最大数量
long keepAliveTime,//线程活动保持时间
TimeUnit unit,//线程活动保持时间单位
BlockingQueue<Runnable> workQueue,//阻塞队列
RejectedExecutionHandler handler//饱和策略)
普通方法
void execute(Runnable command)
在将来某个时候执行给定的任务。
<T> Future<T> submit(Callable<T> task)
提交值返回任务以执行,并返回代表任务待处理结果的Future。
Future<?> submit(Runnable task)
提交一个可运行的任务执行,并返回一个表示该任务的未来。
<T> Future<T> submit(Runnable task, T result)
提交一个可运行的任务执行,并返回一个表示该任务的未来。
1.执行excute方法
public class TestPool {
public static void main(String[] args) {
Runnable myrun=new MyRunn();//创建线程实现类
//核心线程数 最大线程数 线程活动保持时间
ThreadPoolExecutor pool=new ThreadPoolExecutor(3,5,1000,
//线程活动保持时间的单位 饱和策略
TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>());
for(int i=0;i<5;i++){
//用excute()方法提交不需要返回值的任务,不能判断任务是否被线程池执行成功
pool.execute(myrun);//线程池对象调用excute()方法启动线程,不需调用start方法
}
}
}
class MyRunn implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName() + "+" + i);
}
}
}
2.sublmit方法
public class TestPoolSubmit{
public static void main(String[] args) {
Callable callable=new MyCall();//创建线程实现类
//核心线程数 最大线程数 线程活动保持时间
ThreadPoolExecutor pool=new ThreadPoolExecutor(3,5,1000,
//线程活动保持时间的单位 饱和策略
TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>());
for(int i=0;i<5;i++){
Future<String> future = pool.submit(callable);//利用Future对象接收
try {
//通过get(time)方法获取返回值,若指定时间为获取到则返回
//get()方法会等到任务执行完为止。
String str= future.get(1000,TimeUnit.MILLISECONDS);
System.out.println(str);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
class MyCall implements Callable {
@Override
public Object call() throws Exception {
for(int i=0;i<50;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
return Thread.currentThread().getName()+"任务执行完毕";
}
}
下面是一些常用的静态工厂,生成一些常用的线程池:
1.newSingleThreadExecutor
创建一个单线程的线程池,这个线程池只有一个线程在工作,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedTheadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,知道线程达到线程池的最大大小。线程池的大小一旦达到最大值就保持不变,如果某一个线程因为执行异常结束,那么线程池会补充一个新线程。
3.newCachedThreadPool
创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(1分钟不执行)的线程,当任务书增加是,此线程池又可以智能的添加新线程来处理任务,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统能够创建线程的大小。
4.newScheduledThreadPool
创建一个大小无限的线程池,此线程池支持定时以及周期性的执行任务的需求。
关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程, 然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的 区别:
shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执 行任务的列表。
shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程
合理配置线程池
首先需从以下方面分析任务:
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级:高、中和低。
任务的执行时间:长、中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接。
CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任 务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太 大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
注:可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的CPU个数
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优先级高的任务先执行。