使用线程池的好处
- ●降低资源的损耗。重复利用创建好的线程,减少了线程创建和销毁的开销。
- ●提高响应速度。当任务来临时,创建好的线程能立马处理新来的任务,不需要等待线程创建完毕后再执行任务。
- ●方便线程的管理。线程是稀缺资源,过多的创建线程可能会降低系统的稳定性,线程池能控制线程创建的数量,并对其进行监控、管理、调优,从而提高系统稳定性。
线程池的处理流程
- 1.新任务来临时,若线程池的线程数小于基本线程数,则创建新的线程处理新来的任务(此时即便线程池有空闲的线程,也要创建新的线程);反之,则执行下一步。
- 2.当新任务来临,而线程池的线程数大于或等于基本线程数时,新任务会被加入的阻塞队列中等待处理。阻塞队列满了之后,执行下一步。
- 3.当队列满,且新任务来临时,则创建新的线程来执行任务。若线程池里的线程已达到最大线程数(即意味着无法再创建线程),执行下一步。
- 4.通过饱和策略来处理任务。
线程池的使用
线程池的创建
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, runnableTaskQueue, handler);
- ●corePoolSize:基本线程数,代表线程池的基本大小。当线程池里的工作线程数小于基本线程数时,总是会创建新的线程来处理新来的任务。如果调用线程池的prestartAllCoreThreads(),线程池会提前创建好所有的基本线程。
- ●maximumPoolSize:最大线程数,代表线程池允许创建的最大线程数目。当任务队列已满,且已创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。需要注意的是,对于无界队列,该值无效。
- ●keepAliveTime:线程池中除基本线程外,其余空闲线程的保活时间。超过这个时间后,空闲线程将会被终止。
- ●timeUnit:线程保活时间的单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLSECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)。
- ●runnableTaskQueue:任务队列,当线程池里的基本线程都在执行任务时,新来的任务将被添加到任务队列里等待执行。该队列有以下几种选择:
-
- 1.ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO。
-
- 2.LinkedBlockingQueue:基于链表的无界阻塞队列,FIFO,吞吐量通常要高于 ArrayBlockingQueue,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
-
- 3.SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
-
- 4.PriorityBlockingQueue:一个具有优先级的无线阻塞队列。 ●handler:饱和策略,当队列和线程池都满了,新提交的任务通过饱和策略进行处理。饱和策略有以下4种:
-
-
- ①.AbortPolicy:默认的策略,作用是直接抛出异常。
-
-
-
- ②.CallerRunsPolicy:只用调用者所在线程来运行任务。
-
-
-
- ③.DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
-
-
-
- ④.DiscardPolicy:不处理,丢弃掉。
-
向线程池提交任务
可以使用两个方法向线程池提交任务,分别是execute()和submit()方法。
- ●execute():用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功。
- ●submit():用于提交需要返回值的任务,返回值是有个Future类型的对象,通过这个对象可以判断任务是否执行成功,并且通过这个Future类型对象的get()方法可以阻塞当前线程直至任务执行成功,该方法会有返回值。
关闭线程池
可以通过调用线程池的shutdown()或者shutdownNow()方法来关闭线程池。他们的原理都是通过遍历线程池中的线程,然后逐个调用线程的interrupt()方法来中断线程。但这两个方法存在着差别:
- shutdown():该方法中断未执行任务的线程,不中断正在执行任务的线程。
- shutdownNow():该方法中断所有正在执行或已暂停的任务线程。
合理配置线程池
任务的性质可分为:CPU密集型任务、IO密集型任务、混合型任务。线程池的配置需要考虑任务特性。
- ●CPU密集型任务:应分配尽可能小的线程池,如线程池大小配置为CPU核心数+1,如此可避免频繁的线程上下文切换造成的开销。
- ●IO密集型任务:应分配尽可能大的线程池,如线程池大小配置为CPU核心数*2,因为IO密集型任务对CPU的使用较少,创建较多的线程可提高对CPU的利用率。
- ●混合型任务:可以将任务拆分人CPU密集型任务和IO密集型任务,然后分别用两个线程池去处理,只有分完之后的两个任务的执行时间相差不大,那么执行起来比串行执行要高效。当两个任务的执行时间相差较大时,执行快的一方要等待执行慢的一方,在效率上并没有多大提升。
ThreadPoolExecutor运行机制
当ThreadPoolExecutor执行execute()方法增加新任务时,运行机制如下:
- 1.如果线程池里运行的线程少于corePoolSize,即使有空闲线程,也会创建新的线程执行任务。
- 2.如果线程池里运行的线程大于或等于corePoolSize,则把新来的任务加入队列。
- 3.如果队列已满且运行的线程数小于maximumPoolSize,则创建新的线程执行任务。
- 4.当队列已满且运行线程数达到最大值maximumPoolSize时,执行饱和策略。
- 注意:创建新线程需要获取全局锁(该操作开销较大),ThreadPoolExecutor采取上述设计思路,是为了尽可能避免全局锁的获取。一般情况下,当线程池里运行的线程数大于或等于corePoolSize后,任务多被加入队列中等待执行,而不是直接创建线程处理任务,故避免了获取全局锁。