文章目录
上一章
深入学习Java多线程(一)多线程的创建方式、线程的状态、sleep和wait、start和run和线程常用方法等
一、线程池的创建方式
Java 里面线程池的顶级接口是 Executor
,但是严格意义上讲并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService
。
1.1 使用Executors静态方法创建
1.1.1 newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute
将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
1.1.2 newFixedThreadPool
创建一个可重用的固定线程数的线程池
,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
ExecutorService threadPool = Executors.newFixedThreadPool(10);
1.1.3 newScheduledThreadPool
创建一个具有定时定期执行任务功能的线程池
,它可安排在给定延迟后运行命令或者定期
地执行。
package com.lsh.createthread;
import java.time.Instant;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* @author :LiuShihao
* @date :Created in 2021/2/24 1:45 下午
* @desc :创建可定时执行的线程池
*/
public class NewScheduledThreadPool {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
System.out.println(Instant.now());
scheduledThreadPool.schedule(()->{
System.out.println("延时2秒执行---");
System.out.println(Instant.now());
},2, TimeUnit.SECONDS);
}
}
1.1.4 newSingleThreadExecutor
Executors.newSingleThreadExecutor()
返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
package com.lsh.createthread;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author :LiuShihao
* @date :Created in 2021/2/24 1:45 下午
* @desc :创建一个单线程化的线程池
* 单线程化线程池(newSingleThreadExecutor)的优点,串行执行所有任务。
* 如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
* 当shutdown时停止线程
*/
public class NewSingleThreadExecutor {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "睡觉前");
try {
System.out.println(Thread.currentThread().getName() + "睡觉中...");
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println("InterruptedException");
}
System.out.println(Thread.currentThread().getName() + "睡觉后");
});
singleThreadExecutor.shutdown();
System.out.println(Instant.now()+"停止线程");
}
}
1.2 使用ThreadPoolExecutor构造方法创建
在阿里巴巴Java开发手册的编码规约中规定了不允许使用 Executors
去创建线程池,而是通过 ThreadPoolExecutor
的方式,这 样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
通过java.util.concurrent
包下的ThreadPoolExecutor
类的构造方法来创造线程池。
1.2.1 参数解释
corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()
或者prestartCoreThread()
方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize
时,keepAliveTime
才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime
,则会终止,直到线程池中的线程数不超过corePoolSize
。但是如果调用了allowCoreThreadTimeOut(boolean)
方法,在线程池中的线程数不大于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 :由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列。
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
threadFactory:线程工厂,主要用来创建线程;
handler:表示当拒绝处理任务时的策略,有以下四种取值:
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也 塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK 内置的拒绝策略如下:
AbortPolicy
: 直接抛出异常,阻止系统正常运行。CallerRunsPolicy
: 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的 任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。DiscardOldestPolicy
: 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。DiscardPolicy
: 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler
接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler
接口。
示例
//Runtime.getRuntime().availableProcessors() 获得本地运行时的可用处理器个数
ExecutorService executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
50,120L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(10000));
1.2.2 线程池使用的常用方法
Executor
是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的;然后ExecutorService
接口继承了Executor
接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;抽象类AbstractExecutorService
实现了ExecutorService
接口,基本实现了ExecutorService
中声明的所有方法;然后ThreadPoolExecutor
继承了类AbstractExecutorService
。
在ThreadPoolExecutor
类中有几个非常重要的方法:
execute()
submit()
shutdown()
shutdownNow()
execute()
方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
submit()
方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果。 去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future
来获取任务执行结果。
shutdown()
和shutdownNow()
是用来关闭线程池的。
shutdown()
:不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow()
:立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
二、线程池工作过程
-
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
-
当调用
execute()
方法添加一个任务时,线程池会做如下判断:
2.1 如果正在运行的线程数量小于corePoolSize
,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于corePoolSize
,那么将这个任务放入队列;
2.3 如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize
,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize
,那么线程池会抛出异常RejectExecutionException
。 -
当一个线程完成任务时,它会从队列中取下一个任务来执行。
-
当一个线程无事可做,超过一定的时间(
keepAliveTime
)时,线程池会判断,如果当前运行的线程数大于corePoolSize
,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize
的大小。
假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。
因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;
当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;
如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;
然后就将任务也分配给这4个临时工人做;
如果说着14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。
当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。
这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。