在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,服务器在创建和销毁线程上花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。
二、先看一个简单的线程池接口的定义
public interface ThreadPool{
void execute(Job job);//执行一个Job,这个Job必须实现Runnable
void addWorkers(int num);//增加工作者线程
void removeWorker(int num);//减少工作者线程
int getJobSize();//得到正在等待执行的任务数量
void shutdown();//关闭线程池
}
客户端可以通过execute(Job)方法将Job提交入线程池执行,而客户端自身不用等待Job的执行完成。线程池接口还提供了增大/减少工作者线程以及关闭线程池的方法。这里工作者线程代表这一个重复执行Job的线程,有每个客户端提交的Job将进入到一个工作队列中等待工作者线程的处理。可以把线程池理解为工厂,工作者线程理解为机器,Job为工作业务。工厂安排机器去进行工作业务。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
1、线程池基本架构
线程池框架顶层是Executor 接口:其内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。
接着,ExecutorService接口继承了Executor接口,定义了一些生命周期的方法。
ThreadPoolExecutor是java线程池框架(Executor-> ExecutorService)的主要实现类,其中定义了线程池的状态,线程池的任务调度策略,任务饱和策略,线程的创建和销毁策略等。
在看看ThreadPoolExecutor的构造方法
corePoolSize:核心线程数量
maximumPoolSize:线程池里最大线程数量,超过最大线程时候会使用RejectedExecutionHandler
keepAliveTime,unit:线程最大的存活时间
BlockingQueue:阻塞队列,详解可看https://blog.csdn.net/Howinfun/article/details/80744004
threadFactory,用来构造线程池里的worker线程
2、线程池的处理流程如图:
从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:
1)线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3)线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
ThreadPoolExecutor执行execute()方法的示意图:public void execute(Runnable command)
①如果当前运行线程少于核心线程的容量(corePoolSize),就创建新线程执行任务(需要获取全局锁);
②如果运行线程等于或多于核心线程容量(corePoolSize),就将任务加入阻塞队列(BlockQueue);
③如果阻塞队列已满,创建新线程处理任务(需要获取全局锁);
④如果创建新线程将使当前运行的线程超过最大线程数maximumPoolSize,任务将被拒绝,调用RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采取上述总体设计思路,是为了执行execute()方法时,尽可能避免获取全局锁,如果当前运行线程数大于等于corePoolSize,几乎所有的execute方法都是执行步骤2,不需要获取全局锁。
工作线程:线程池创建线程时,会将线程封装成工作线程worker,worker执行完任务后,还会循环获取工作队列的任务来执行。
①execute方法创建一个线程时,会让这个线程执行当前任务
②这个线程执行完上图1的任务后,会反复从BlockingQueue获取任务执行
1、Java通过Executors提供四种线程池,分别为:
1)、newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2)、newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,表示同一时刻只能有这么大的并发数
3)、newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
4)、newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
线程池不建议使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors各个方法的弊端:
(1)、 newFixedThreadPool和newSingleThreadExecutor:
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
(2)、newCachedThreadPool和newScheduledThreadPool:
主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
其实看源码就就知道Executors的底层就是ThreadPoolExecutor。只不过加了很多限制
2、通过ThreadPoolExecutor来创建一个线程池:根据构造方法创建线程池
new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, milliseconds, runnableTaskQueue, handler);
(1)corePoolSize(核心线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于核心线程池基本大小时就不再创建。
②runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列,有以下几个选择:
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue无界
③maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。无界队列没有效果
④ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
⑤RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。JDK1.5提供了4中策略,当然也可以自定义策略。
3、向线程池提交任务,主要两种方法execute()方法和submit()方法
(1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
threadsPool.execute(new Runnable() {
@Override
public void run() {
....
}
});
(2) submit()方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的 get() 方法来获取返回值,get()方法会阻塞当前线程直到任务完成。
4、关闭线程池:调用shutdown()或者shutdownNow方法来关闭线程池,实现的原理是逐个调用线程的interrupt方法来中断线程。只要调用了这两个方法的任意一个,isShutdown方法就会返回true。shutdownNow方法会立刻关闭线程池,即使有的任务还没有执行完。
六、合理配置线程池:分析任务特效。
七、线程池的监控
如果系统中存在大量使用的线程池,则有必要进行监控,方便在出现问题时进行定位处理。可以通过参数进行监控:
taskCount:线程池需要执行的任务数
completeTaskCount:已完成的任务数
largestPoolSize:线程池里曾经创建过的最大线程数量,可以知道线程池是否满过。
getPoolSize:线程池的线程数量
getActiveSize:获取活动的线程数量
借鉴文章:https://blog.csdn.net/huangwei18351/article/details/82975499
书籍:《java并发编程的艺术》