近期研究了一下java自带的线程池工具Executo,所以记录一下学习的内容。
什么是线程池?
线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,即均为启动,不消耗CPU,而只是占用较小的内存空间。当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以*创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。
先说一下为什么要使用线程池?
因为大部分web任务都是量大而数据小的业务,若服务器端每次都开辟一个新的线程来处理客户端请求的话,会消耗很多的系统资源,因为创建和销毁线程是很花费的时间和消耗的系统资源的,若创建过多的线程会导致系统过渡消耗内存活着“切过度”。线程池是应对上述问题所出现的产物,重用已开辟的线程来减少服务端的开销,也消除了创建新线程时时间上的消耗,提升了响应请求的速度。
总结一下有以下两点:
1.减少了创建和销毁线程的次数,每个工作的线程都可以被重复利用,可重复利用执行多个任务。
2.可以根据系统的承载能力,调整线程池中工作线程的数目,防止因消耗过多内存,而导致系统宕机,其实就是像扮演了一个有界缓冲区的角色,若果流量瞬间出现了抖动,连接池会使他变得平缓,而不是消耗更多的系统资源。
Executor为java5中出现的线程池API,线程池的*接口,只是一个执行线程的工具,他有一个execute方法,用于执行runnable对象。而真正的线程池接口是executorService,从下图可以看到executorService的提供了管理、运行Executor的api。
Executors类为使用者提供了一系列创建线程池的工厂方法,该方法创建的线程池都返回executorService接口。
Executors类中主要提供了以下三种线程池对象。
1. newCachedThreadPool()创建一个可以根据需要扩充的线程池,它会对线程进行自动回收。
2. newFixedThreadPool()创建一个固定大小的线程池。
3. newSingleThreadExecutor()创建一个单例线程的执行器。保证任务的执行顺序且在任意给定的时间都不会有多个线程同时活动。
可以看下面的例子就能够理解了
//测试线程类
public class MyThread implements Runnable{
private int number;
public MyThread(int num){
number = num;
System.out.println("Create Thread:"+num);
}
public void run(){
System.out.println("Thread:"+number+" is running");
//为了让实验效果更明显
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread:"+number+" end");
}
}
//主程序
public class ThreadPool {
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(2);
for(int i = 0; i < 5;i++){
//execute方法接受一个runnable对象作为参数,以异步的形式去执行他
exec.execute(new MyThread(i));
}
exec.shutdown();
}
}
newFixedThreadPool(2)其实为开辟一个线程数目为2的线程池,实验结果如下
可以看到execute方法先加载了五个线程任务,但是并不是马上执行他们,因为线程池中只有2个能够执行任务的线程,所以每次只能有两个任务执行,只有当执行中的任务执行完毕后,才能执行接下来的任务。
newCachedThreadPool的执行结果如下
可以看到newCachedThreadPool所创建的线程池并不需要我们指定线程池中所能执行任务的数量,他会根据我们所传入的任务量自动进行设定。
newSingleThreadExecutor顾名思义,线程池每次只执行一个任务:
从上图可以看到,虽然所有任务都被递交了,但是线程池每次只执行一个任务,只有当上一个任务执行完时,下一个任务才会继续执行。
看完实例后我们来看一下上述三种线程池工厂方法的源码。
1.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
2.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
3.newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看到实际创造线程池的是ThreadPoolExecutor方法,只是初始化ThreadPoolExecutor时的参数不一样,就会生成功能不同的线程池。那么ThreadPoolExecutor方法是何许人也?我们看一下它的源码。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
}
这里有以下几个参数:
1. corePoolSize :线程池的基本大小,即线程池创建后,即使没有任务递交,也会生成相应数量的线程,只是线程不会工作,而是处于空闲状态。
2. maximumPoolSize:线程池所能执行任务的最大数目,也就是线程池所能创建的线程数量的最大值。
3. keepAliveTime:若工作线程闲置后,则会判断空闲线程的时间是否超出了keepAliveTime,若超出则会抛弃。
4. unit:keepAliveTime的时间单位
5. workQueue:workQueue为阻塞型先进先出队列,若任务数目大于corePoolSize,则会存入队列中,等到执行中的任务执行完时会去队列中取任务来执行。
现在说一下ThreadPoolExecutor的执行策略:
1.运行时,若向线程池中增加新任务,每一个Thread 都是新任务,从实例中可以看到所有任务都会被添加,但是并没有马上的执行。
2.此时当前运行线程数量小于corePoolSize的数量,则任务直接运行,无需放入workQueue排队。
3.若当前运行线程数量大于等于corePoolSize的数量,小于maximumPoolSize的数量,则向workQueue队列中添加任务。
4.若workQueue队列满了,则新建线程会直接执行,若此时总线程数量大于maximumPoolSize的数量,则会调用RejectedExecutionHandler来拒绝任务。
RejectedExecutionHandler则是在线程池饱和状态情况下对新进任务处理的策略,一共有四种。