线程池,顾名思义就是若干个线程的集合。在程序中通过使用线程池,减少cpu和内存的使用。节省线程的创建和销毁时间,下面将对线程池进行介绍。
一、线程池工作原理
举个例子,创建一个核心线程数为10,最大线程数为15的线程池,当一个任务提交,首先会判断当前线程数是否达到核心线程数也就是10,如果达没有达到,将会创建线程来执行任务,如果达到核心线程数,将会判断队列已满,如果未满,则会将任务添加到队列等待执行,如果满了,判断线程池是否已满线程数达到15,如果没满,创建非核心线程来执行任务。如果线程池已满,将会执行拒绝策略。
1、拒绝策略
(1) AbortPolicy:中断抛出异常
(2)DiscardPolicy:默默丢弃任务,不进行任何通知
(3)DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
(4) CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)
2、队列
(1)SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
(2)LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
3、源码分析
添加任务并启动执行
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
二、线程池的优点:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
缺点 - 重复制作线程系统的费用很大,每个线程的制作和销毁都需要时间,任务比较简单的话,制作和销毁线程消耗的资源有可能比线程执行任务本身消耗的资源大。
- 过多的线程占有过多的内存等资源,在带来过多的上下文切换的同时,系统也会变得不稳定。
三、 java线程池四种创建方式比较
在java中有如下四种的线程池创建方式:
1、创建一个固定大小的线程池
public class FixedThreadPoolTest {
public static void main(String[] args) {
//1.创建一个大小为5的线程池
ExecutorService threadPool= Executors.newFixedThreadPool(5);
//2.使用线程池执行任务一
for (int i=0;i<5;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务1"));
}
//3.使用线程池执行任务二
for (int i=0;i<10;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务2"));
}
//4.使用线程池执行任务二
for (int i=0;i<20;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务2"));
}
}
}
2、创建大小不限制的线程池,基于缓存,提交任务立即执行
public class CachedThreadPoolTest {
public static void main(String[] args) {
//1.创建一个大小为5的线程池
ExecutorService threadPool= Executors.newCachedThreadPool();
//2.使用线程池执行任务一
for (int i=0;i<5;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务1"));
}
//2.使用线程池执行任务二
for (int i=0;i<8;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务2"));
}
}
}
3、创建单个线程的线程池
public class SignleThreadPoolTest {
public static void main(String[] args) {
//1.创建一个大小为5的线程池
ExecutorService threadPool= Executors.newSingleThreadExecutor();
//2.使用线程池执行任务一
for (int i=0;i<5;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务1"));
}
//2.使用线程池执行任务二
for (int i=0;i<8;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务2"));
}
}
}
4、创建基于定时任务的线程池
public class ScheduledThreadPoolTest {
public static void main(String[] args) {
//1.创建一个大小为5的线程池
ExecutorService threadPool= Executors.newScheduledThreadPool(10);
//2.使用线程池执行任务一
for (int i=0;i<5;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务1"));
}
//2.使用线程池执行任务二
for (int i=0;i<8;i++){
//给线程池添加任务
threadPool.submit(() -> System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务2"));
}
}
}
二、线程池创建源码分析
1、 参数说明:
(1)corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
(2)maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
(3)keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
(4)unit:keepAliveTime的时间单位
workQueue:用于保存任务的队列,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
(5)threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
(6)handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、(7)DiscardOldestPolicy、DiscardPolicy
无论以何种方式创建线程池,初始化线程池参数都会调用ThreadPoolExecutor类的构造器方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
实际的赋值操作如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
2、四种创建方式参数比较:
(1)newCachdThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
通过参数可以看到,创建大小不限制大小的线程池,核心线程数初始化为0,因为线程池创建完成,没有提交任务的话,没有任务。线程池的最大大小为
@Native public static final int MAX_VALUE = 0x7fffffff;
换算下来是2^31-1,线程空闲时间默认是60秒。队列是同步消息队列没有拒绝策略。
(2)newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
创建线程池,初始化参数的时候,核心线程数,最大线程数都是一样的。线程空闲停留时间是0s,队列是链表实现的阻塞消息队列。
3、创建单个线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
核心线程数和最大线程数是一样的,空闲时间是0s,队列是阻塞消息队列,类型Rrunnable,线程停留时间是毫秒。
4、创建基于定时任务调度的线程池
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
传入参数是核心线程数,其他参数在队列中设置,最大线程数没有做限制,线程空闲时间是0s,队列是延迟消息队列。
综上所述,四种创建线程池的比较归纳如下: