【2023】java多线程之线程池讲解汇总(包括代码举例)
一、JAVA使用线程池主要的三个原因
- 创建/销毁线程需要消耗系统资源,线程池可以复用创建的线程。
- 控制并发的数量。并发数量过多,可能导致资源消耗过多,从而造成服务器崩溃。(主要原因)
- 可以对线做统一管理。
线程池的创建方式总共有7种,总体来说分为2类:
1. 通过 ThreadPoolExecutor 创建的线程池;
2. 通过 Executors 创建的线程池。
二、线程池创建方式:
- Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
- Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
- Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置,后⾯会详细讲。
#
添加任务的方式:
*使用submit可以执行有返回值的任务或者是无返回值的任务;而execute只能执行不带返回值的任务。 *****
使用线程工厂创建线程:
提供的功能:
1. 设置(线程池中)线程的命名规则。
2. 设置线程的优先级。
3. 设置线程分组。
4. 设置线程类型(用户线程、守护线程)。
三、创建线程池
1. 创建固定大小线程池
2. 创建可缓存的线程池,感觉线程任务数量创建
- 优点:在一定时间内可以重复使用这些线程,产生相应的线程池。
- 缺点:适用于短时间有大量任务的场景,它的缺点是可能会占用很多的资源。
3. 创建延时定时任务线程池
- scheduleAtFixedRate 是以上⼀次任务的开始时间,作为下次定时任务的参考时间的(参考时间+延迟任务=任务执⾏)。*
- scheduleWithFixedDelay 是以上⼀次任务的结束时间,作为下次定时任务的参考时间的。*
- schedule():可以设置延时时间,定时执行;
4. 创建单线程线程池
- 创建可抢占式线程池
⭐6. 使用ThreadPoolExecutor创建
ThreadPoolExecutor参数说明:
拒绝策略
- JDK4种+1种自定义策略
具体代码
执行流程
- 线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到corePoolSize,在核心线程数量 <
corePoolSize时)。注意,这一步需要获得全局锁。- 线程总数量 >= corePoolSize时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(体现了线程复用)。
- 当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。注意,这一步需要获得全局锁。
- 缓存队列满了, 且总线程数达到了maximumPoolSize,则会采取上面提到的拒绝策略进行处理。
阻塞队列
我们假设一种场景,生产者一直生产资源,消费者一直消费资源,资源存储在一个缓冲池中,生产者将生产的资源存进缓冲池中,消费者从缓冲池中拿到资源进行消费,这就是大名鼎鼎的生产者-消费者模式。
该模式能够简化开发过程,一方面消除了生产者类与消费者类之间的代码依赖性,另一方面将生产数据的过程与使用数据的过程解耦简化负载。
我们自己coding实现这个模式的时候,因为需要让多个线程操作共享变量(即资源),所以很容易引发线程安全问题,造成重复消费和死锁,尤其是生产者和消费者存在多个的情况。另外,当缓冲池空了,我们需要阻塞消费者,唤醒生产者;当缓冲池满了,我们需要阻塞生产者,唤醒消费者,这些个等待-唤醒逻辑都需要自己实现。
BlockingQueue是Java util.concurrent包下重要的数据结构,区别于普通的队列BlockingQueue提供了线程安全的队列访问方式,并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
BlockingQueue一般用于生产者-消费者模式,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。BlockingQueue就是存放元素的容器。
阻塞队列提供了四组不同的方法用于插入、移除、检查元素:
方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() | - | - |
阻塞队列:BlockingQueue workQueue
- 维护着等待执行的Runnable任务对象。
LinkedBlockingQueue
- 链式阻塞队列,底数据结构是链表,默认大小是Integer.MAX_VALUE,也指定大小
ArrayBlockingQueue
- 数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
SynchronousQueue
- 同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
DelayQueue
- 延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
PriorityBlockingQueue
- 无界阻塞队列,优先级的判断通过构造函数传入的Compator对象来决定。
阻塞队列的原理
阻塞队列主要是利用了Lock锁的多条件(Condition)阻塞控制。
首先是构造器,除了初始化队列的大小和是否是公平锁之外,还对同一个锁(lock)初始化了两个监视器,分别是notEmpty和notFull。这两个监视器的作用目前可以简单理解为标记分组,当该线程是put操作时,给他加上监视器notFull,标记这个线程是一个生产者;当线程是take操作时,给他加上监视器notEmpty,标记这个线程是消费者。
还没有评论,来说两句吧...