学习笔记-线程池
一、线程池概念
线程的使用带来了很多好处,比如异步调用,提升性能等,然而,频繁的创建和销毁线程会耗费计算机资源。
类似于数据库连接池,这类带‘池’的功能很好地解决了上述问题。
线程池的核心是创建n个线程放在容器中,等到有任务处理时会将其分配给其中某个线程,执行完之后不会立即销毁,而是放回容器中,等待下个任务执行。
综上,线程池具备如下优点:
1.避免了重复创建和销毁线程,降低了系统资源消耗
2.执行任务时无需重新创建,而是直接从池中获取线程,缩短了执行时间
3.使得线程资源控制成为可能
二、线程池创建
1 public class ThreadPoolTest { 2 3 public void print(){ 4 System.out.println("当前线程" + Thread.currentThread().getName()); 5 } 6 7 public static void main(String[] args) { 8 ExecutorService service = Executors.newFixedThreadPool(3); 9 for (int i = 0; i < 1000; i++) { 10 service.execute(new Thread(()-> new ThreadPoolTest().print())); 11 }service.shutdown(); 12 } 13 }
这里用的是jdk中提供的api——Executors.newFixedThreadPool(3)创建线程池,该方法会创建一个带有3个线程大小的线程池。
java中提供的以下创建线程池的API
线程池 | 功能 |
newFixedThreadPool | 创建一个固定大小的线程池 |
newSingleThreadExecutor | 创建一个只有一个线程的线程池 |
newCachedThreadPool | 创建一个根据实际情况调整线程数的线程池 |
newScheduledThreadPool | 创建带有延迟和周期性执行功能的线程池 |
阅读源码不难发现这些api实际上都调了ThreadPoolExecutor类的构造函数,区别在于参数各不相同。
这里选择其中最简单的构造方法相关参数(有些参数是可以通过构造方法传入的,如threadFactory)说明,以此来了解其中差异。
线程池 |
corePoolSize (核心线程数) |
maximumPoolSize (最大线程数) |
keepAliveTime (存活时间) |
unit (单位) |
workQueue (保存任务的队列) |
threadFactory (创建线程使用的工厂) |
handler (拒绝策略) |
newFixedThreadPool | n | n | 0 | 毫秒 | LinkedBlockingQueue | DefaultThreadFactory(默认,可通过构造方法传入) | AbortPolicy(默认) |
newSingleThreadExecutor | 1 | 1 | 0 | 毫秒 | LinkedBlockingQueue | DefaultThreadFactory(默认,可通过构造方法传入) | AbortPolicy |
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60 | 秒 | SynchronousQueue | DefaultThreadFactory(默认,可通过构造方法传入) | AbortPolicy |
newScheduledThreadPool | n | Integer.MAX_VALUE | 0 | 纳秒 | DelayedWorkQueue | DefaultThreadFactory(默认,可通过构造方法传入) | AbortPolicy |
1.newFixedThreadPool:核心线程数和最大线程数相同,为用户指定的值,不存在工作线程,因此无论有多少任务过来,线程池中线程最大只能是n,不会耗费多余资源创建线程,适用于负载比较大的场景下对线程资源的管控;
2.newSingleThreadExecutor:核心线程数和最大线程数为1,由此创建的是单线程的线程池。
3.newCachedThreadPool:无核心线程,最大线程为integer的最大值,线程空闲60秒之后便会销毁。这是一种伸缩性很强的线程池,当大量任务执行时会不断创建线或使用空闲线程来执行,当任务执行完之后又能全部销毁。
4.newScheduledThreadPool:继承了ThreadPoolExecutor,并另外提供一些调度方法以支持定时和周期任务。
三、线程池执行流程
4、线程池使用
线程池的创建因业务,机器配置等不同而不同,线程池的创建应该符合当前业务场景,使用ThreadPoolExecutor灵活度更高,不建议使用Executors创建线程池。
4.1 配置线程池大小
首先分析线程池中任务的特性,是CPU密集型还是IO密集型。
CPU密集型:线程数=CPU核心数
IO密集型:一般可配置核心数的两倍,线程池设定最佳线程数目=((线程池设定的线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目
4.2 execute和submit的区别
- execute只可以接收一个Runnable参数,submit可接收Runnable和Callable两种参数
- execute会抛出异常,submit不会,除非调用Future.get();
- execute没有返回值,submit有返回值
4.3 拒绝策略
- AbortPolicy : 直接抛出异常,这是默认策略
- CallerRunsPolicy : 用调用者所在线程来执行任务
- DiscardOldestPolicy : 丢弃阻塞队列中最靠前的任务,并执行当前任务
- DiscardPolicy : 直接丢弃任务
4.4 队列的选择
- LinkedBlockingQueue是一个无界队列,因此不会触发拒绝策略,适合存放重要,不能丢弃的任务,另外,无界也可以应对高并发场景
- ArrayBlockingQueue是有界队列,当线程池中线程数达到corePoolSize,且任务队列已满,同时线程数未超过maxPoolSize时会创建工作线程执行。因此通过ArrayBlockingQueue和maxPoolSize配置使用可以控制线程资源,适合重要性低的任务场景
- SynchronousQuene只存放一个任务的队列,当队列满且有新任务时,若线程数未超过maxPoolSize会创建新线程执行。因此可以通过此队列可以为每个新来的任务分配一个线程,与maxPoolSize配合使用可以控制线程数量,适合处理高并发且实时性要求高的场景,但可能会耗尽系统资源
- PriorityBlockingQueue具有优先级的无界阻塞队列