线程池的好处
- 降低资源消耗。通过重复利用已有的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行
- 提高线程可管理性。线程是稀缺资源,使用线程池进行统一分配、调优和监控,可以降低资源消耗,提高系统稳定性
线程池的7个构造参数
- corePoolSize(线程池的基本大小)
当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即时其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建
2. maximumPoolSize(线程池最大数量)
线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用无界阻塞队列做任务队列,则这个参数没有什么效果
3. keepAliveTime(线程活动保持时间)
线程池的工作线程空闲后,保持存活的时间。如果任务很多,并且每个任务的执行时间都比较短,可以调大时间,提高线程利用率
4. unit(线程保持活动时间的单位)
可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微妙(MICROSECONDS)和纳秒(NANOSECONDS)
5. workQueue(任务队列)
用于保存等到执行的任务的阻塞队列,可以选择以下几个阻塞队列:
- ArrayBlockingQueue 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序
- LinkedBlockingQueue 一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素,吞吐量通常高于 ArrayBlockingQueue,并且没有边界
- SynchronousQueue 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一致处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue
- PriorityBlockingQueue 一个具有优先级的无界阻塞队列。
6. threadFactory 用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字 7. 拒绝策略 当任务和线程池都满了,说明线程池处于饱和状态,必须采取一种策略处理提交的新任务。在 JDK5 中线程池框架提供了以下四种策略: 也可以根据需要实现 RejectedExecutionHandler 接口自定义策略
- AbortPolicy:直接抛出异常,默认采取这种策略
- CallerRunsPolicy:使用调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
线程池提交任务流程
- 任务提交后,线程池会判断和核心程数是否已经满了,如果没有满,则创建线程执行任务。
- 如果核心线程数已经满了,那么判断任务队列是否满了,如果任务队列没有满,则将任务添加到任务队列
- 如果任务队列已经满了,那么判断当前线程池中线程数是否小于最大线程数,如果小于最大线程数,那么创建线程执行任务。
- 如果线程数已经已经等于最大线程数,那么该任务不能被线程池处理,执行对应的拒绝策略。
线程池生命周期
- 线程池正常运行时,调用
shutdown方法,线程池处于shutdown状态,此时,线程池不再接受新的任务,会继续处理任务队列中的任务和线程正在处理的任务。 - 如果直接调用shutdownNow方法,线程池不仅不会接受新的任务,并且当前执行的任务和任务队列中的任务都会停止执行,此时线程池处于stop状态,并返回没有执行的任务列表。
- 线程池中任务都停止了,工作线程也全部结束,即tinying状态。
- terminated状态,线程池已经销毁。
线程池是如何实现线程复用的?
我们都知道,正常情况下,线程执行完
run方法,线程就需要被java虚拟机回收,那么为什么线程池中的线程是怎么实现复用的呢? 先说答案,其实很简单,就是不让run方法执行完,通过死循环加阻塞的形式,对线程实现复用。
线程执行java任务核心代码如下:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 不断尝试从任务队列getTask()方法获取任务,如果没有获取到任务,阻塞当前线程
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 获取到任务的线程执行任务的run方法
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
线程池中,将每个线程包装成了Worker对象,HashSet中,worker会不断从阻塞队列中尝试获取任务,如果获取成功,执行任务,如果没有获取到任务,阻塞队列会阻塞当前线程,将当前线程挂起。等待阻塞队列中有任务了,再次将线程唤醒,(:这个模式是不是跟我们学习的生产者消费者模式像的一批?)
JDK提供的5种线程池
- newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
核心线程数和最大线程数都是1,阻塞队列是一个无界的LinkedBlockingQueue。该线程池适用于任务顺序执行,缺点但是不能充分利用CPU多核性能 2. newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数和最大线程数是相等的,阻塞队列也是无界的LinkedBlockingQueue,该线程池适用于能估算出需要多少核心线程数量的场景 3. newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心线程数为0,最大线程数为int最大值,可以理解为无穷大,线程没有任务执行存活时间为60S,阻塞队列是SynchronousQueue。这种阻塞队列没有容量,功能相当于转发任务,该线程池任务来临时会不断的创建新的worker线程,所以,比较适合执行大量,轻量级任务 4. newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
该线程池的阻塞队列是一个延迟队列,该延迟队列内部维护了一个大根堆,可以对任务进行排序,该线程池适合执行定时任务或者延迟任务。 5. newWorkStealingPool
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
这个是JDK1.8新出的一个线程池,内部使用了ForkJoin技术,可以对任务进行拆分,然后多个线程并行执行,然后将结果进行汇聚,类似于算法中的分治思想。
线程池线程数设置
方式1 - 经验之谈
一般线程池执行的任务可以分为两种,一种是IO密集型,一种是CPU密集型。
- IO密集型:CPU核心数*2 因为IO不占CPU的资源,可以多开启一些线程
- CPU密集型:CPU核心数 + 1 生产环境中,很难通过计算获得具体的线程数设置,因为干扰因素有很多,详见:mp.weixin.qq.com/s/G0toGUjKQ…
方式2 - 任务量化
可以精确算出核心线程数,最大线程数,阻塞队列大小等信息。我没有看明白,感觉没有把CPU核数考虑进去,自己认为不太对,大家感兴趣的可以看一下www.cnblogs.com/waytobestco…