一、为什么要用线程池
一个线程执行某个任务的时间片可以分成3块,创建线程T1,执行任务T2,销毁线程T3,现实中往往是 T1+T3>T2,也就是真正执行任务花费的时间很短,反而是创建与销毁线程更耗时,也是就是若我们需要多个线程去执行任务时,线程的创建和销毁会占用更多资源。于是有人提出提前创建一堆线程,然后把它们放在一个容器中统一进行管理,需要用的时候就直接拿出来用,用完之后再放回池子里。这样就不会在线程的创建和销毁上浪费时间。
上面的“池子”就是线程池,很明显线程池可以给我们带来很多好处:
- 低资源消耗,降低了频繁创建线程和销毁线程的开销
- 提高响应速度
- 提高线程的可管理性,可以对线程进行一些操作,方便管理线程
池化技术有很多, 比如线程池
、数据库连接池
、HTTP连接池
等等。
线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。(创建的线程,实际最后要和操作系统的线程做映射,很消耗资源)
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
二、线程池中的核心参数
这个池子给我们带来很多好处,但这个池子不是没有边界的,需要一些参数来限制这个池子。
corePoolSize
核心线程的最大个数,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。
maximumPoolSize
线程池最大线程数,它表示在线程池中最多能创建多少个线程。线程数量超过这个值就会抛异常。
keepAliveTime
表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是allowCoreThreadTimeOut(true)方法可以使得线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
unit是参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue
任务队列,是一个阻塞队列(需要等上一个任务处理完),用来存储等待执行的任务,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue。
ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列,,此队列按 FIFO(先进先出)原则对元素进行排序
SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。静态工厂方法Executors.newCachedThreadPool使用了这个队列。
LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
threadFactory
线程工厂,主要用来创建线程。
handler
表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
三、线程的任务处理流程
当在execute(Runnable)方法中提交新任务并且少于corePoolSize线程正在运行时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多于corePoolSize但小于maximumPoolSize线程正在运行,则仅当队列已满时才会创建新线程。 通过设置corePoolSize和maximumPoolSize相同,您可以创建一个固定大小的线程池。 通过将maximumPoolSize设置为基本上无界的值,例如Integer.MAX_VALUE,您可以允许池容纳任意数量的并发任务。 通常,核心和最大池大小仅在构建时设置,但也可以使用setCorePoolSize和setMaximumPoolSize进行动态更改。
四、 Executor、ExecutorService、Executors
Executor 是一个抽象层面的核心接口,它定义了execute()方法,用来接收一个Runnable接口的对象。
ExecutorService 接口继承了Executor 接口,是Executor 的子接口。ExecutorService 接口对 Executor 接口进行了扩展,提供了返回 Future 对象,终止,关闭线程池等方法。当调用 shutDown 方法时,线程池会停止接受新的任务,但会完成正在 pending 中的任务。Executor接口中execute()方法不返回任何结果,而ExecutorService接口中submit()方法可以通过一个 Future 对象返回运算结果。通过 ExecutorService.submit() 方法返回的 Future 对象,还可以取消任务的执行。Future 提供了 cancel() 方法用来取消执行 pending 中的任务。
Executors 类提供了若干个静态方法,用于生成不同类型的线程池。但线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,下面将介绍。
五、ThreadPoolExecutor
讲完了上面的核心参数就可以看看怎么创建线程池了,ThreadPoolExecutor是线程池的核心类,有4个构造方法可以得到我们需要的线程池:
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) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
六、常见的线程池及其使用场景
在 Executors 类里面提供了一些静态工厂,生成一些常用的线程池。
newFixedThreadPool
创建固定大小的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
特点
核心线程数
==最大线程数
(没有救急线程被创建),因此也无需超时时间- 阻塞队列是无界的,可以放任意数量的任务
- 适用于任务量已知,相对耗时的任务
newCachedThreadPool(推荐使用)
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
特点
- 没有核心线程,最大线程数为
Integer.MAX_VALUE
,所有创建的线程都是救急线程 (可以无限创建),空闲时生存时间为60秒 - 阻塞队列使用的是SynchronousQueue,没有容量,没有线程来取是放不进去的,只有当线程取任务时,才会将任务放入该阻塞队列中
- 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
使用场景:
- 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。 任务执行完毕,这唯一的线程也不会被释放。
- 和自己创建单线程执行任务的区别:自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而
newSingleThreadExecutor
线程池还会新建一个线程,保证池的正常工作 Executors.newSingleThreadExecutor()
线程个数始终为1,不能修改
newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
- 1)newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。 - 2)newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
在『任务调度线程池』功能加入之前,可以使用java.util.Timer来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
@Slf4j(topic = "guizy.TestTimer")
public class TestTimer {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");
Sleeper.sleep(2);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
// 使用timer添加两个任务, 希望他们都在1s后执行
// 由于timer内只有一个线程来执行队列中的任务, 所以task2必须等待task1执行完成才能执行
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
}
}
08:21:17.548 guizy.TestTimer [Timer-0] - task 1
08:21:19.553 guizy.TestTimer [Timer-0] - task 2
使用 ScheduledExecutorService:
public class TestTimer {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.schedule(() -> System.out.println("任务1, 执行时间:" + new Date()), 1000, TimeUnit.MILLISECONDS);
executor.schedule(() -> System.out.println("任务2, 执行时间:" + new Date()), 1000, TimeUnit.MILLISECONDS);
}
}
任务1, 执行时间:Sun Jan 03 08:53:54 CST 2021
任务2, 执行时间:Sun Jan 03 08:53:54 CST 2021
七、如何合理配置线程池的大小
线程池究竟设成多大是要看你给线程池处理什么样的任务,任务类型不同,线程池大小的设置方式也是不同的。
任务一般可分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。
- CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。额外的一个线程用于应对偶尔的任务阻塞或意外的上下文切换。 - IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。
IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。 - 混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
怎么合理设置线程池的参数?
为了说明合理设置的条件,我们首先确定有以下几个相关参数:
1.tasks,程序每秒需要处理的最大任务数量(假设系统每秒任务数为100~1000)
2.tasktime,单线程处理一个任务所需要的时间(每个任务耗时0.1秒)
3.responsetime,系统允许任务最大的响应时间(每个任务的响应时间不得超过2秒)
corePoolSize:
每个任务需要tasktime秒处理,则每个线程每秒可处理1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime)。即tasks*tasktime个线程数。假设系统每秒任务数为100到1000之间,每个任务耗时0.1秒,则需要100x0.1至1000x0.1,即10到100个线程。那么corePoolSize应该设置为大于10。
具体数字最好根据二八原则,即80%情况下系统每秒任务数,若系统80%的情况下任务数小于200,最多时为1000,则corePoolSize可设置为20。
queueCapacity:任务队列的长度
队列的大小决定了系统能够缓冲多少任务。队列过大可能导致任务等待时间过长,队列过小则可能导致频繁地创建新线程。
任务队列的长度通常设计为:核心线程数除以单个任务执行时间再乘以2,即 queueCapacity = corePoolSize / 任务执行时间 * 2。例如,如果核心线程数为10,单个任务执行时间为0.1秒,则队列长度可以设计为200。
如果队列长度设置过大,会导致任务响应时间过长,如以下写法:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。
maxPoolSize:最大线程数
当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么一个任务需要的线程数=20/200=0.1,那么当每秒达到1000个任务时,则需要==(1000-queueCapacity)*(20/200)==,即60个线程,可将maxPoolSize设置为60。
keepAliveTime:
线程数量只增加不减少也不行。当负载降低时,可减少线程数量,如果一个线程空闲时间达到keepAliveTime,该线程就退出。默认情况下线程池最少会保持corePoolSize个线程。keepAliveTime设定值可根据任务峰值持续时间来设定。
以上关于线程数量的计算并没有考虑CPU的情况。若结合CPU的情况,比如,当线程数量达到50时,CPU达到100%,则将maxPoolSize设置为60也不合适,此时若系统负载长时间维持在每秒1000个任务,则超出线程池处理能力,应设法降低每个任务的处理时间(tasktime)。
八、怎么理解无界队列和有界队列
有界队列
就是有固定大小的队列。比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue。
无界队列
指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。比如没有设定固定大小的 LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。
常见的有界队列为:
- ArrayBlockingQueue 基于数组实现的阻塞队列
- LinkedBlockingQueue其实也是有界队列,但是不设置大小时就时Integer.MAX_VALUE,内部是基于链表实现的
- SynchronousQueue
比较奇葩,内部容量为零,适用于元素数量少的场景,尤其特别适合做交换数据用
常见的无界队列:
- ConcurrentLinkedQueue无锁队列,底层使用CAS操作,通常具有较高吞吐量,但是具有读性能的不确定性,弱一致性——不存在如ArrayList等集合类的并发修改异常,通俗的说就是遍历时修改不会抛异常
- PriorityBlockingQueue 具有优先级的阻塞队列 DelayedQueue 延时队列
- LinkedTransferQueue 简单的说也是进行线程间数据交换的利器
九、线程池是有哪些放弃策略?
- AbortPolicy:ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常也不处理
- CallerRunsPolicy:在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务;主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,这样可以有效降低向线程池内添加任务的速度。
- DiscardPolicy:采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。
- DiscardOldestPolicy:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务加入。
建议大家用 CallerRunsPolicy 策略,因为当队列中的任务满了之后,如果直接抛异常,那么这个任务就会被丢弃。如果是 CallerRunsPolicy 策略,则会用主线程去执行,也就是同步执行,这样操作最起码任务不会被丢弃。
#十、自定义一个简单的线程池
- 阻塞队列中维护了由主线程(或者其他线程)所产生的的任务
- 主线程类似于生产者,产生任务并放入阻塞队列中
- 线程池类似于消费者,得到阻塞队列中已有的任务并执行
自定义线程池的实现步骤 :
- 步骤1:自定义拒绝策略接口
- 步骤2:自定义任务阻塞队列
- 步骤3:自定义线程池
- 步骤4:测试
@FunctionalInterface // 接口中只有一个方法,加上这个注解,实现lambda简写
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue, T task);
}
public class BlockingQueue<T> {
// 1、任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2、锁
private ReentrantLock lock = new ReentrantLock();
// 3、生产者的条件变量 (当阻塞队列塞满任务的时候, 没有空间, 此时进入条件变量中等待)
private Condition fullWaitSet = lock.newCondition();
// 4、消费者的条件变量 (当没有任务可以消费的时候, 进入条件变量中等待)
private Condition emptyWaitSet = lock.newCondition();
// 5、阻塞队列的容量
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
// 获取队列大小
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
// 从阻塞队列中获取任务, 如果没有任务,会一直等待
public T take() {
lock.lock();
try {
// 阻塞队列是否为空
while (queue.isEmpty()) {
// 进入消费者的条件变量中等待,此时没有任务供消费
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 阻塞队列不为空, 获取队列头部任务
T t = queue.removeFirst();
fullWaitSet.signal(); // 唤醒生产者进行生产, 此时阻塞队列没有满
return t;
} finally {
lock.unlock();
}
}
// 从阻塞队列中获取任务, 如果没有任务, 会等待指定的时间
public T