线程池
1.原理
1.1为什么使用线程池?
线程池体现了一种池化的思想,有些类似于mysql的连接池。我们在使用多线程的时候,阿里规约会建议我们使用线程池,而不是直接new Thread。我认为线程池主要体现在以下几个优点:
- 便于管理线程,比如,可以自定义控制线程的创建数量。因为可以实现线程的统一分配,调用及监控。
- 降低系统的资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗。
- 提高响应的速度,这里主要体现在任务到达时,不用再等待线程创建便可以直接调用。
1.2 线程池原理
线程池的主要执行流程如下:
ThreadPoolExecutor执行示意图如下:
-
corePool核心线程池是否已满,如果未满,则创建新线程来执行任务。
-
corePool已满,则将任务存至BlockingQueue
工作队列中。 -
若创建后的线程数量将大于maximumPoolSize,则调用RejectedExecutionHandler的策略来处理此类未执行的任务,若创建线程后的数据仍然小于等于maximumPoolSize,则直接创建线程来执行任务。
以上三步中,只有第二步不会要求获取全局锁。在设计上已经尽力避免进行获取全局锁的这个消耗操作,所以一般在第二步的时候就已经可以处理完成任务了。
为什么设计的时候会区分核心线程池和最大线程池呢?
个人理解是为了追求性能与资源消耗的极致吧,假如核心线程池足以满足任务执行的需求,那就不必再创建核心线程池之外的线程,同时,真的出现线程不够用的时候,同样也会给它一个机会来创建一个线程
执行。尽量不出现无法执行任务的情况。只是个人猜想,望指正。
2.使用
2.1 创建
构造方法
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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
-
corePoolSize:核心线程池大小。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使存在空闲的核心线程能够执行任务,其仍会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
-
maximumPoolSize:最大线程数大小。线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
-
keepAliveTime:线程存活时间。这里是指工作线程在执行任务进入空闲状态的时候。当有需要任务需要频繁执行的时候,可以调节此参数来使线程存活时间增加,避免频繁创建新线程造成资源的浪费。
-
TimeUnit:线程存活时间单位。
-
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字 。
-
workQueue:任务工作队列。用于保存等待执行的任务的阻塞队列。有以下几种:
-
ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO的原则进行元素的排序。
-
LinkedBlockingQueue: 基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
-
SynchronousQueue:不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列,个人理解此队列为了保证每个任务尽可能的被执行处理。
-
PrirorityBlockingQueue:具有优先级的无限阻塞队列。
-
-
RejectedExecutionHandler:饱和策略。当工作队列和线程池无法接收或执行任务时,则采用一种策略来处理提交的任务。
-
AbortPolicy:直接抛出异常
-
CallerRunsPolicy:只用调用者所在线程来运行任务。
-
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
-
DiscardPolicy:不处理任务,直接丢弃掉。
-
2.2 执行
向线程池提交任务主要有两个方法:
-
execute()方法
提交一个不需要返回值的任务,所以无法判断任务是否被线程池执行。例如:
threadPoolExecutor.execute(new Runnable() { @Override public void run() { //do something } });
-
submit()方法
可以返回任务执行的结果,通过Future对象来包装执行的结果,Future对象可以获取任务是否执行成功,任务执行的结果等。
Future future = threadPoolExecutor.submit(()->{ try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); });
2.3 关闭
使用shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断
线程,因此无法响应中断的任务可能永远无法终止。
二者的区别是,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任
务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程,算是我们常说的优雅的
关闭方式。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调
用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方
法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
2.4 配置
技术离不开业务需求,所以在线程池的配置上也离不开实际的需求。比如:当需要处理提交频繁的任务时,我们可以尝试将线程的存活时间调大。再或者,如果为了保持应用的稳定性,我们可以使用有界的阻塞队列,避免队列中线程任务不断积压,导致内存崩溃。
参考:《Java并发编程的艺术》