线程池的优点
- 线程是稀缺资源,使用线程池可以减少创建销毁线程的次数,每个工作线程都可以重复使用。
- 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。
线程池的创建
创建时,有多个构造方法,参数个数不同,最终都调用下面的构造方法进行创建。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数含义:
- corePoolSize:线程池核心线程数量
- maximumPoolSize:线程池最大线程数量
- keepAliveTime:当活跃线程数大于核心线程数时,空闲的多余线程的最大存活时间
- unit:存活时间的单位
- workQueue:存放任务的队列
常用的workQueue类型:
- SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大。
- LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程小于核心线程数,则新建线程(核心线程)处理任务,如果当前线程数等于核心线程数,则进入队列等待调度。由于这个队列默认设置容量为
Integer.MAX_VALUE
,即超过核心线程数的任务都将加载到队列中,这个队列会导致maximumPoolSize参数设置失效,因为线程池中线程数永远等于corePoolSize。 - ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize值,则新建核心线程执行任务,如果达到了,则入队等待,如果队列已满,则新建非核心线程执行任务,如果达到了总线程数,并且队列也满了,则报错。
- DelayQueue:队列内元素必须实现Delayed接口,这就意味着你穿进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先入队,达到指定的延时时间才执行任务,相当于延时队列。
- threadFactory:创建线程的方式。
- handler:超出线程范围和队列容量的任务的处理程序
任务拒绝策略:
1、ThreadPoolExecutor.AbortPolicy
:丢弃任务并抛出RejectedExecutionException异常。
2、ThreadPoolExecutor.DiscardPolicy
:也是丢弃任务,但是不抛出异常。
3、ThreadPoolExecutor.DiscardOldestPolicy
:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
4、ThreadPoolExecutor.CallerRunsPolicy
:由调用线程处理该任务
常见线程池
CachedThreadPool()
可缓存线程池,线程数无限制(Integer.MAX_VALUE
),有空闲线程则复用空闲线程,若无则新建线程
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
FixedThreadPool()
定长线程池,可控制线程最大并发数,超过线程数的任务在队列中等待。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
ScheduledThreadPool()
有时间延时的线程池,这个线程池间接的利用ThreadPoolExecutor创建。支持定时及周期性任务执行。任务队列使用的是延时队列,可以规定时间延迟。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
SingleThreadExecutor()
单线程线程池,核心线程和最大线程数都为1,所有任务按指定顺序执行,遵循队列的入队出队规则。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
SingleThreadScheduledExecutor()
单线程可延时线程池,该线程池结合了ScheduledThreadPool和SingleThreadExecutor两个线程池的内容。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
线程池的实现原理
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池中的核心线程是否都在执行任务,如果没有则创建一个新的工作线程来执行任务。如果核心线程数已经达到创建限制,并且核心线程都在执行任务,则进行下个流程。
2、线程池判断任务队列是否已满,如果任务队列未满,则将任务提交存储到任务队列中。如果任务队列满了,则进入下个流程。
3、判断线程池中的线程是否都处于工作状态,如果没有,则创建一个工作线程(非核心线程)来执行任务,如果这时,线程都在工作且线程数达到线程池最大线程数量,则交给饱和策略来处理这个任务(handler)。
源码分析
下面写一个简单例程进行源码分析的入口,测试demo如下:
public class ThreadPoolTest {
public static void main(String[] args) {
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue);
for (int i = 0; i < 16; i++) {
poolExecutor.execute(()->{
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("线程池中活跃的线程数:"+ poolExecutor.getPoolSize());
if (queue.size() > 0){
System.out.println("-------------------任务队列阻塞的线程数"+queue.size());
}
}
poolExecutor.shutdown();
}
}
- 首先查看线程池ThreadPoolExecutor类的execute方法,可以看到注释内容即是线程池实现原理。具体几步解释见下面中文解释。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
//判断线程数量是否小于核心线程数
if (workerCountOf(c) < corePoolSize) {
//创建线程并启动
if (addWorker(command, true))
return;
c = ctl.get();
}
//向任务队列中提交任务
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//提交成功后,进行校验,判断线程池是否运作中,如果线程池没有运行中,则移除插入的任务,然后提交给饱和策略
if (! isRunning(recheck) && remove(command))
reject(command);
//判断可用线程数是否等于0,是,开启工作线程执行任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//尝试开启工作线程,如果失败,则提交给饱和策略
else if (!addWorker(command, false))
reject(command);
}
- 接下来,我们查看addWorker方法,创建线程的方法。该方法有两个参数,而第二个core参数,表示的创建的是核心线程还是非核心线程,传参为boolean型。
- 如下图第一个框内,是进行判断,判断线程池状态大于等于shutdown(即状态不是运行中),并且再满足后面的条件,则不处理直接返回。
- 第二个框中,是更新线程池线程数量,根据传入的core 参数,判断是否为核心线程,如果为true,且线程数小于corePoolSize,则跳出循环,创建核心线程。
3.接下来的代码如下图,第一个框中,将任务封装到Worker类中,并获得线程池主锁,保证线程安全。在这里,线程池中的线程是通过Worker类来实现的。
4.第二个框中,再次校验线程池状态和刚创建的线程状态,接下来将线程加入线程池中(workes)。
5.第三个框中,启动刚创建的线程。
- 下面我们查看Worker类的run方法,run方法中调用了runWorker方法,源码如下图。
- 第一个框中,当该线程是刚创建第一次执行任务,或者从任务队列中获取到任务,首先判断线程池状态,如果线程池不是停止状态,清除当前线程的中断标志后再次判断线程池状态;如果前者判断为true,接下来再次判断当前线程是否为中断,再结合前者条件,true:将当前线程中断,false:执行当前线程。
- 第二个框中,执行任务开始前操作钩子。
- 第三个框中,执行任务。
- finally块中,执行执行任务后钩子。
在上述简单demo中,运行后会出现如下错误,因为我们设置的LinkedBlockingQueue队列的容量是5,核心线程为5,最大线程数为10,for循环将开启总共16个线程执行16次任务。当线程池中线程数达到最大线程数10后,开始往队列中存储任务,存储5个,队满后,线程数和队列容量都达到了临界点,所以执行饱和策略,由于未定义,所以执行默认的RejectedExecutionException
异常。
线程池中活跃的线程数:7
-------------------任务队列阻塞的线程数5
线程池中活跃的线程数:8
-------------------任务队列阻塞的线程数5
线程池中活跃的线程数:9
-------------------任务队列阻塞的线程数5
线程池中活跃的线程数:10
-------------------任务队列阻塞的线程数5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at threadPools.ThreadPoolTest.main(ThreadPoolTest.java:19)
接下来,我们定义一个丢弃策略,在创建线程池时将该参数传入,再次执行将不会出现上述异常,因为丢弃策略是将超出限制的线程丢弃不执行。除此之外,还可以自定义其他饱和策略来处理当线程池达到饱和状态的情况。
public class ThreadPoolTest {
public static void main(String[] args) {
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
//丢弃策略,多余的丢弃,不报错
ThreadPoolExecutor.DiscardPolicy handler = new ThreadPoolExecutor.DiscardPolicy();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue,handler);
for (int i = 0; i < 16; i++) {
poolExecutor.execute(()->{
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("线程池中活跃的线程数:"+ poolExecutor.getPoolSize());
if (queue.size() > 0){
System.out.println("-------------------任务队列阻塞的线程数"+queue.size());
}
}
poolExecutor.shutdown();
}
}