线程池
创建线程的方法:()
-
继承 thread 类(缺点:线程类已经继承了 Thread 类无法继承其他类了,如果一个类已经有父类)、
-
实现 runnable 接口(可以解耦)、使用线程池
3.实现callable接口。FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务
池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销
好处
- 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 第三:**提高线程的可管理性。**线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- 第四:Java1.5 中引入的 Executor 框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService,Executors 是一个框架,ThreadPoolExecutor 是线程池实现的核心类。
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 ExecutorService,Executors 是一个框架,ThreadPoolExecutor 是线程池实现的核心类。
可以使用 Executors 框架中的静态工厂方法之一来创建一个线程池:(如果线程池中抛出异常,内部会被 try,catch 掉。可以通过 future.get()捕获异常)
- newFixedThreadPool:创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再变化(如果某个线程由于发生了未预期 Exception 而结束,那么线程池会补充一个新的线程)。
- newCachedThreadPool:创建一个可缓存的线程池,如果线程池当前规模超过了处理需求,那么将回收空间线程,而当需求增加时,可以添加新线程,线程池规模不存在任何限制(不推荐使用,线程池规模不可控)大小为 Integer.MAX_VALUE,2 的 31 次方-1。(线程数量过多可能导致OOM)
- newSingleThreadExecutor**:是一个单线程的 Executor**,如果这个线程异常结束,会创建另一个线程替代,它能保证依照任务在队列中的顺序串行执行(FIFO, LIFO, 优先级)
- newScheduledThreadPool:创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于 Timer。
ExecutorService threadPool = Executors.newFixedThreadPool(3);
ExecutorService threadPool = Executors.newCachedThreadPool();
ExecutorService threadPool = Executors.newSingleThreadExecutor();
threadPool.execute(Runnable r);
threadPool.shutdown();//将任务执行完毕后关闭
threadPool.shutdownNow();//**立即关闭,并返回没有执行的任务(List< Runnable >)
对于计算密集型的任务,在拥有 N 个 cpu 的系统上,当线程池的大小为 N+1 时,通常能实现最优利用率。对于 I/O 密集型,通常线程池大小为 2*N
Java.util.concurrent.ThreadPoolExecutor
ThreadPoolExecutor 类 是 线 程 池 中 最 核 心 的 一 个 类 , Executors 中 的newFixedThreadPool 等方法就是由 ThreadPoolExecutor 实现。不过在 java doc 中,并不提倡我们直接使用 ThreadPoolExecutor,而是使用 Executors 类中提供的几个静态方法来创 建 线 程 池 。 ThreadPoolExecutor 继 承 AbstractExecutorService 类 , 即ThreadPoolExecutor 中能够调用ExecutorService 的方法
ThreadPoolExecutor 中主要方法,execute(Runnable command)、submit(Callable< T >task)、shutdown()、shutdownNow();
execute 和 submit 都属于线程池的方法,对比:
- execute 只能执行 Runnable 类型的任务**,没有返回值; submit 既能提交 Runnable 类型任务也能提交 Callable 类型任务,底层是封装成FutureTask(FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况),然后调用 execute 执行**
- execute 会直接抛出任务执行时的异常,submit 会吞掉异常,可通过Future 的 get 方法将任务执行时的异常重新抛出
submit 底层还是调用 execute
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200,TimeUnit.MILLISECONDS,new ArrayBlockingQueue< Runnable >(5));
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize 个线程或者一个线程。**默认情况下,**在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize 后,就会把到达的任务放到缓存队列当中;
-
maximumPoolSize:最大线程数,当队列中存放的任务达到队列容量时,当前可以同时运行的数量变为最大线程数,创建线程并立即执行最新的任务,与核心线程数之间的差值又叫救急线程数
-
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。但是如果调用了 allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize 时,keepAliveTime 参数也会起作用,直到线程池中的线程数为 0;
-
unit:
keepAliveTime
参数的时间单位,- 参数 keepAliveTime 的时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
- 参数 keepAliveTime 的时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性:
-
workQueue:阻塞队列,存放被提交但尚未被执行的任务
一般来说,这里的阻塞队列有以下几种选择-
ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;(强制有界)
-
LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE;
-
synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
一般使用 LinkedBlockingQueue 和 Synchronou
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较:
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
-
-
threadFactory:线程工厂,创建新线程时用到,可以为线程创建时起名字
-
handler:拒绝策略,线程到达最大线程数仍有新任务时会执行拒绝策略
-
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。默认策略
-
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
-
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
补充:其他框架拒绝策略
- Dubbo:在抛出 RejectedExecutionException 异常前记录日志,
- 并 dump 线程栈信息,方便定位问题
- Netty:创建一个新线程来执行任务
- ActiveMQ:带超时等待(60s)尝试放入队列
- PinPoint:它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
-
使用流程
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NaMijvGx-1679668652094)(https://typora963.oss-cn-hangzhou.aliyuncs.com/JUC-%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86-1673425907078523-167673650029625.png)]
-
创建线程池,这时没有创建线程(懒惰),等待提交过来的任务请求,调用 execute 方法才会创建线程
-
当调用 execute() 方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列
- 如果这时队列满了且正在运行的线程数量还小于 maximumPoolSize,那么会创建非核心线程立刻运行这个任务(救急线程),对于阻塞队列中的任务不公平。这是因为创建每个 Worker(线程)对象会绑定一个初始任务,启动 Worker 时会优先执行
- 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
-
当一个线程完成任务时,会从队列中取下一个任务来执行
-
当一个线程空闲超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后最终会收缩到 corePoolSize 大小
实现原理
ThreadPoolExecutor 中有一个实现了 Runnable 接口的 Worker 内部类,可以 new Worker 类,构造函数中传入任务,在 Worker 类中创建新线程,Worker 重写 Runnable的 run 方法,while (task != null || (task = getTask()) != null),getTask 方法从workQueue 中取出任务。即循环不断取出任务,while 循环中 run 任务时要 lock。当不满足while 循环中的条件,即没有任务,getTask 调用 queue 的 take 方法,会阻塞。
在这个 while循环中【while (task != null || (task = getTask()) != null)中】,会调用 task(即传递进来的 runnable)的 run 方法,而不是 start 开启新线程,因为 while 循环在 worker 类的 run 方法中,已经是一个线程了。
在 addWorker 方法中 new 的 Worker 类。
在 execute()方法中,当小于 corePoolSize 时,调用 addWorker 方法并传入任务。
Executors 框架:(和前面讲的联系起来)
newFixedThreadPool 创建的线程池 corePoolSize 和 maximumPoolSize 值是相等的,它使用的 LinkedBlockingQueue;
newSingleThreadExecutor 将 corePoolSize 和 maximumPoolSize 都设置为 1,也使用的LinkedBlockingQueue;
newCachedThreadPool 将 corePoolSize 设 置 为 0 , 将 maximumPoolSize 设 置 为Integer.MAX_VALUE,使用的 SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过 60 秒,就销毁线程。
ScheduledThreadPoolExecutor 继 承 了 ThreadPoolExecutor , 传 入 队 列 是DelayedWorkQueue()。
队列补充()
有界队列和无界队列:
- 有界队列:有固定大小的队列,比如设定了固定大小的LinkedBlockingQueue,又或者大小为 0
- 无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出(超过 Integer.MAX_VALUE),所以相当于无界
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:FIFO队列
- ArrayBlockQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的无界(默认大小Integer.MAX_VALUE)的阻塞队列
- PriorityBlockQueue:支持优先级排序的无界阻塞队列
- DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞
到有一个 put 的线程放入元素为止 - LinkedTransferQueue:由链表结构组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
与普通队列(LinkedList、ArrayList等)的不同点在于阻塞队列中阻塞添加和阻塞删除方法,以及线程安全:
- 阻塞添加 put():当阻塞队列元素已满时,添加队列元素的线程会被阻塞,直到队列元素不满时才重新唤醒线程执行
- 阻塞删除 take():在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般会返回被删除的元素)
对于任务耗时较短的情况下,线程数不宜过多。 对于任务耗时长的情况,分为 I/O 密集和计算密集。I/O 密集的时候,相对来说 CPU 空闲的时间比较多,那可以适当的增加线程数,增加 CPU 的利用率。 计算密集的时候,CPU 一直被占用进行计算,线程数不宜太多。