池技术
什么是池?
汉字的基本释义:
池塘;旁边高中间洼的地方等
在网络技术范畴内,池(Pool)的概念被广泛的应用在服务器端软件的开发上。
使用池结构相比于不使用池结构的基本优势分析
在衔接节点上,无池结构时需要不断的创建和销毁新服务对象必将给造成系统资源的巨大开销,导致系统的性能下降,甚至系统资源耗尽
使用池结构可以减少不必要的新服务对象的创建和销毁,用一个容器保存着各种需要的对象。对这些对象进行复用,从而降低系统资源开销、提高程序的响应速度、改善效率。
常用的池技术有:Socket连接池、JDBC连接池、线程池等。
本质上池是在资源使用时将使用率高且可以复用的对象保存在一个类似容器的地方,对这些对象进行复用,从而降低非必须的系统资源开销、提高程序的响应速度、改善效率。
线程池剖析
线程池的生命周期及其使用
1.创建
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
milliseconds,runnableTaskQueue, handler);
corePoolSize(线程池的基本大小)提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用
移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,默认为Integer.MAX_VALUE2147483647。静态工厂方法Executors.newCachedThreadPool使用了这个队列
PriorityBlockingQueue:一个具有优先级的无限阻塞队列
ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线
程设置有意义的名字,代码如下
new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build()
RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状
态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法
处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
·AbortPolicy:直接抛出异常。
·CallerRunsPolicy:只用调用者所在线程来运行任务。
·DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
·DiscardPolicy:不处理,丢弃掉。
当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务
2.提交
execute()用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
submit()提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
3.关闭
shutdown将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
线程池的类型
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。队列-SynchronizedQueue存放大小1
newFixedThreadPool定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
队列-LinkedBlockingQueue-无界导致maxNumPoolSize无效
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
合理使用线程池
从几个不同角度来判断和分析
任务的性质:
CPU密集型任务-应配置尽可能小的线程数量,如配置N*cpu+1个线程的线程池
【获取cpu核数:Runtime.getRuntime().availableProcessors()】
IO密集型任务-线程并不是一直在执行任务,则应配置尽可能多的线程
混合型任务-如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量
将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
任务的优先级
可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。
执行时间不同
可以交给不同规模的线程池来处理,或者可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越
长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。线程池的监控
线程池的实现原理
当一个任务提交时(excute())
1.线程池判断核心线程池里的线程是否都在执行任务,若不是(少于corePoolSize)则创建一个工作线程在执行任务(需要获取全局锁),如果已满,则进入下一步
2.线程池判断工作队列是否已满,如果没满则将任务线程添加入工作队列(BlockingQueue),否则进去下一步
3.判断线程池里的线程是否都处于工作状态,不是(小于maximumPoolSize)则创建新线程来完成任务(需要获取全局锁),否则交给饱和策略[RejectExcutionHandle.rejectedExcution()]来处理这个任务
这些步骤是为了避免使用全局锁[可能是一个严重的可伸缩瓶颈]
工作线程:线程池创建线程时,会将线程封装成工作线程worker,worker在执行任务完成之后会循环从阻塞队列里获取任务来执行。