线程池的实现原理
概念
线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
为什么需要线程池:线程分为两种状态,有用户态和内核态,而Java线程依赖于内核线程,线程创建和销毁的时候都要进行用户态和内核态的切换,线程的上下文切换,消耗资源。可以使用线程池(可以理解为线程缓存池)重复利用线程执行多个任务,减少资源消耗。
使用线程池的好处:
1.减少在创建和销毁线程上所花的时间以及系统资源的开销
2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行
3.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存
线程池的处理流程

1. 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2. 如果运行的线程等于或多于corePoolSize,则将任务加入Blocking-Queue。
3. 如果无法将任务加入Blocking-Queue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
4. 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution () 方法。

注意:ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。
线程池的使用
创建
创建线程池有两种方式
// 第一种方式——快捷方式 ThreadPoolExecutor pool = (ThreadPoolExecutor) Executors.newFixedThreadPool(10); // 第二种方式——标准方式 // 设定各种参数 ThreadPoolExecutor pool2 = new ThreadPoolExecutor(args);
1. 快捷创建线程池很简单,只需要调用Executors中相应的静态工厂方法即可,比如:
Executors.newFixedThreadPool(int nThreads)
| newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
| newSingleThreadExecutor() | 创建只有一个线程的线程池 |
| newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。小程序使用这个快捷方法没什么问题,对于服务器需要长期运行的程序,创建线程池应该直接使用ThreadPoolExecutor 的构造方法。
2. 使用ThreadPoolExecutor 创建线程池
// Java线程池的完整构造函数 public ThreadPoolExecutor( int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle(空闲)状态,也不会回收。 int maximumPoolSize, // 线程数的上限 long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长, // 超过这个时间,多余的线程会被回收。 BlockingQueue<Runnable> workQueue, // 任务的排队队列 ThreadFactory threadFactory, // 新线程的产生方式 RejectedExecutionHandler handler) // 拒绝策略
// 传入指定参数即可创建 new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,milliseconds, runnableTaskQueue, handler)
参数解释:
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务书大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择一下几个阻塞队列:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法-Executors.newFixedThreadPool () 使用了这个队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool 使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
- maximumPoolSize(线程池的最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
- ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
- RejectedExecutionHandler(饱和策略\拒绝策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK1.5中Java线程池框架提供了以下4种策略:
-
AbortPolicy:直接抛出异常(默认策略)。
-
CallerRunsPolicy:只用调用者所在线程来运行任务。
-
DiscardOldestPolicy:丢弃队列里最老的一个任务,尝试为当前提交的任务腾出位置 。
-
DiscardPolicy:不处理,丢弃掉。
当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
- AliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。、
- TimeUnit(线程保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS),千分之一微秒。
以运营一家装修公司做个比喻。
公司在办公地点等待客户来提交装修请求;公司有固定数量的正式工以维持运转;旺季业务较多时,新来的客户请求会被排期,比如接单后告诉用户一个月后才能开始装修;当排期太多时,为避免用户等太久,公司会通过某些渠道(比如人才市场、熟人介绍等)雇佣一些临时工(注意,招聘临时工是在排期排满之后);如果临时工也忙不过来,公司将决定不再接收新的客户,直接拒单。
线程池就是程序中的 “装修公司”。
向线程池提交任务
1. 可以向线程池提交的任务有两种:Runnable和Callable,Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。二者的区别如下:
-
方法签名不同,void Runnable.run(), V Callable.call() throws Exception
-
是否允许有返回值,Callable 允许有返回值。
-
是否允许抛出异常,Callable 允许抛出异常。、
2. 可以使用两个方法向线程池提交任务,分别为execute()和submit()方法。
- execute()只能提交Runnable类型的任务,无返回值,所以无法判断任务是否执行成功。且在执行任务时,如果遇到异常会直接抛出。
- submit()既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,通过这个future对象可以判断任务是否执行成功;但当任务类型是Runnable时,返回值为null。(这与Callable和Runnable接口要重写的函数有关)。并且在执行任务时,遇到异常不会直接抛出,只有在使用Future的get方法获取返回值时才会抛出异常。
- future的get方法在任务为执行完未获得返回值(这个返回值就是执行call()完成后的返回值)之前会一直阻塞,可以使用future的isDone()方法判断任务是否执行完成,然后再决定是否get。
- 如果使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候任务有可能还未执行完。

1 public class TestThreadPoolBegin { 2 3 public static void main(String[] args) throws Exception{ 4 ExecutorService es = Executors.newSingleThreadExecutor(); 5 Callable callable = new Callable() { 6 @Override 7 public String call() throws Exception { 8 System.out.println("线程处理开始..."); 9 int a = 2; 10 int b = 3; 11 System.out.println("3/2的结果为:" + b/a); 12 System.out.println("线程处理结束..."); 13 return "0"; 14 } 15 }; 16 Future<String> future = es.submit(callable); 17 while(true) { 18 //idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。 19 if(future.isDone()) { 20 System.out.println("任务执行完成:" + future.get()); 21 break; 22 } 23 } 24 es.shutdown(); 25 } 26 }
更多示例见:https://www.cnblogs.com/jxxblogs/p/11882381.html
获取结果
1. 获取单个结果
通过submit()向线程池提交任务后会返回一个Future类型的对象,调用V Future的get()方法能够阻塞等待任务执行的结果,V get(long timeout, TimeUnit unit)方法可以指定等待的超时时间。
2. 获取多个结果
- 如果向线程池提交了多个任务,要获取这些任务的执行结果,可以依次调用Future的get()获得。
- 但对于这种场景,我们更应该使用ExecutorCompletionService,该类的take()方法总是阻塞等到某一个任务的完成,然后返回该任务的Future对象。向CompletionService批量提交任务后,只需调用相同次数的take()方法,就能获取所有任务的执行结果,获取顺序是取决于任务的完成顺序。
1 void solve(Executor executor, Collection<Callable<Result>> solvers) 2 throws InterruptedException, ExecutionException { 3 4 CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 构造器 5 6 for (Callable<Result> s : solvers)// 提交所有任务 7 ecs.submit(s); 8 9 int n = solvers.size(); 10 for (int i = 0; i < n; ++i) {// 获取每一个完成的任务 11 Result r = ecs.take().get(); 12 if (r != null) 13 use(r); 14 } 15 }
关闭线程池
两种方法:shutdown或shutdownNow
原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
区别:
- shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
- shutdown将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminated 方法会返回 true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow 方法。
线程池的相关类
-
Executor 是一个顶层接口(类似一个标记接口),在它里面只声明了一个方法 execute (Runnable),返回值为 void,参数为 Runnable 类型,从字面意思可以理解,就是用来执行传进去的任务的;
-
ExecutorService 接口继承了 Executor 接口,并声明了一些方法:submit、invokeAll、invokeAny 以及 shutDown 等;这个是线程池真正的接口。
-
抽象类 AbstractExecutorService 实现了 ExecutorService 接口,基本实现了 ExecutorService 中声明的所有方法;
-
ThreadPoolExecutor 继承了类 AbstractExecutorService。
线程池状态
1. RUNNING:这是最正常的状态,接收新的任务,处理等待队列中的任务。线程池的初始化状态时RUNNING。线程池一旦被创建,就处于RUNNINNG状态,并且线程池中的任务数为0.
2. SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown方法时,线程池由RUNNING -> SHUTDOWN。
3. STOP:不接受新的任务的提交,也不再处理等待队列中的任务。中断正在执行任务的线程。调用线程池的shotdownNow方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4. TIDYING:所有的任务都销毁了,workCount为0,线程池的状态在转换为TIDYING时会调用terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理,可以通过重载terminated()函数实现。
5. TERMINATED:线程池处于TIDYIN状态时,执行完terminated()之后,就会由TIDYING -> TERMINATED。
注:
- 当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN->TIDYING.
- 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
合理配置线程池(合理设置线程池大小)
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- CPU密集型任务:主要是执行计算机任务,需要大量的运算,而没有阻塞,CPU一直全速运行,这种任务cpu的利用率很高,线程个数为CPU核数。这几个线程可以并行执行,不存在线程切换的开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗。注意只有在真正的多核CPU上才可能得到加速,在单核CPU上,无论开几个模拟线程,该任务都不可能得到加速。
- IO密集型任务:即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要是利用了被浪费掉的阻塞时间。
性质不同的任务可以交给不同规模的线程池执行:
- CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数;
- IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1;
- 混合型任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,就没必要拆分了。
若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时等待的时间越长,则CPU空闲的时间越长,那么线程数应设置得越大,才能更好的利用CPU。
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
可以得出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。
可以通过 Runtime.getRuntime ().availableProcessors () 方法获得当前设备的 CPU 个数。
浙公网安备 33010602011771号