1 背景
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。
2 解决方法
在Java中可以通过线程池来避免频繁创建销毁线程、实现重复利用,其实际上就是容纳了多个线程的容器。这种思想是不是非常类似于我们生活中的共享单车呢?
3 线程池优势
- 降低资源消耗。减少了创建和销毁线程的次数。
- 提高响应速度。任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而导致服务器挂掉。
4 java中的线程池
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
Executors类中常用的创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:创建的是有界线程池,也就是池中的线程个数可以指定最大数量。newCachedThreadPool()
:创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中。newSingleThreadExecutor()
:创建一个只有单线程的线程池,它相当于调用newFixedThreadPool()方法时传入参数为1。newScheduledThreadPool(int corePoolSize)
:创建具有指定线程数的线程池,它可以在指定延迟后执行线程任务。corePoolSize指池中所保存的线程数,即使线程是空闲的也被保存在线程池内。newSingleThreadScheduledExecutor()
:创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。ExecutorService newWorkStealingPool(int parallelism)
:创建持有足够的线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争。ExecutorService newWorkStealingPool()
:该方法是前一个方法的简化版本。如果当前机器有4个CPU,则目标并行级别被设置为4,也就是相当于为前一个方法传入4作为参数。
获取到线程池ExecutorService 对象后,调用如下方法使用线程池对象:
-
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行(Future接口:用来记录线程任务执行完毕后产生的结果。) -
<T> Future<T> submit(Callable<T> task)
: 获取线程池中的某一个线程对象,并执行(Future : 表示计算的结果) -
V get()
: 获取计算完成的结果。
使用线程池中线程对象的步骤:
- 创建线程池对象
Executors.newFixedThreadPool()
。 - 创建Runnable接口子类对象
new MyRunnable() 或 new MyCallable()
。(task) - 提交Runnable接口子类对象
submit()
。(take task) - 关闭线程池
shutdown() 或 shutdownNow()
(前者使线程池状态由RUNNING变为SHUTDOWN,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务;后者使线程池状态由RUNNING变为STOP,不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程)。
线程池的工作流程:
线程池中的重要参数:
corePoolSize
(核心工作线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时。maximumPoolSize
(最大线程数):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。keepAliveTime
(多余线程存活时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。workQueue
(队列):用于传输和保存等待执行任务的阻塞队列。threadFactory
(线程创建工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。handler
(拒绝策略):当线程池和队列都满了,再加入线程会执行此策略
线程池的拒绝策略:
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
AbortPolicy
:丢弃任务并抛出RejectedExecutionException异常。DiscardPolicy
:也是丢弃任务,但是不抛出异常。DiscardOldestPolicy
:丢弃队列最前面的任务,然后重新尝试执行任务(重复该过程)。CallerRunsPolicy
:由调用线程处理该任务。
线程池的状态:
一共有五种状态, 分别是:
RUNNING
:能接受新提交的任务,并且也能处理阻塞队列中的任务。SHUTDOWN
:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于RUNNING
状态时,调用shutdown()
方法会使线程池进入到该状态。STOP
:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于RUNNING
或SHUTDOWN
状态时,调用shutdownNow()
方法会使线程池进入到该状态。TIDYING
:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用terminated()
方法进入TERMINATED
状态。TERMINATED
:在terminated()
方法执行完后进入该状态,默认terminated()方法是空实现,可以重写该方法进行相应的处理。
下图为线程池的状态转换过程:
线程池的队列大小通常怎么设置?
-
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。 -
IO密集型任务:
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。 -
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。