剑指Offer(锁)——Java线程池

在Web开发中,服务器需要接受并且处理请求,所以会为一个请求分配一个线程去处理,如果并发的请求数量很大,但是请求的时间很短,那么就会频繁的创建和销毁线程,造成额外的CPU隐患,如此一来,会大大降低系统的效率。

为了解决上述的问题,于是我们可以利用Executors创建不同的线程池满足不同场景的需求,目前这个类提供了五种创建线程池的方法:

  1. newFixedThreadPool(int nThreads):指定工作线程数量的线程池
  2. newCachedThreadPool():处理大量短时间工作任务的线程池,

(1)试图缓存线程并重用,当无缓存线程可用的时候,就会创建出新的工作线程
(2)如果线程限制的时间超过阈值,则会被终止并移除缓存
(3)系统长时间闲置的时候,不会消耗什么资源
3. newSingleThreadExecutor():创建唯一的工作者线程来执行任务,如果线程异常结束,会有课另一个线程取代他
4. newSingleThreadScheduledExecutor()newScheduledThreadPool(int corePoolSize):定时或者周期性的工作调度,两者的区别在于单一线程还是多个线程
5. newWorkStealingPool:内部创建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。

为了了解第五条所说的ForkJoinPool,这里就来介绍一下Fork/Join框架:

  • Fork/Join框架

这是Java7提供的一个并行处理任务的框架,它能把大人物分割成若干个小人物,最终汇总每一个任务结果后得到大人物结果的框架。

可以将递归的方法,拆成一个一个小的任务,分治解决。

从原理上来说,和MapReduce的原理是一样的。

它会将任务分发给任务池中的工作线程,使用工作窃取即WorkStealing算法(某个线程从其他队列里窃取任务来执行)。

上面我们提到,Fork/Join就是将大任务切割成小任务去执行,最后将小任务的结果整理起来得到大人物的结果,它会对这些任务分配单独的队列,单独的线程去执行,那么,这里就会出现一种情况,有些线程的队列任务已经完成,但是有些队列的任务还没有完成,所以,WorkStealing算法,就体现了作用,它能在线程处理任务之后,窃取busy线程的任务去执行,但是为了防止出现竞争任务的情况,一般我们都采用双端队列。

被窃取任务的队列永远都从头部获取执行,窃取任务的队列永远从尾部获取执行。

剑指Offer(锁)——Java线程池_第1张图片

  • 为什么要使用线程池
  1. 重复使用线程,降低资源消耗
    2.提高线程的可管理性,提高工程的可管理性

接下来我们就来介绍一下Executor的框架:

Executor框架是一组执行策略调用,资源调度和控制的异步任务框架,目的是将任务提交和任务如何运行分离开来的机制。
剑指Offer(锁)——Java线程池_第2张图片

而JUC下一共有三个Executor接口

  • Executor

他的作用可能是创建一个新线程并且立刻使用,也可能是使用已有的运行线程运行当前的任务,也有可能是视设置线程池的容量或者阻塞队列的容量来决定是否将传入的线程放入阻塞队列中,或者拒绝接受传入的线程
剑指Offer(锁)——Java线程池_第3张图片
我们之前创建单线程去跑的时候是这样的:

Thread t = new Thread();
t.start();

但是使用了Executor之后,就会变成这样

Thread t = new Thread();
executor.execute(t);
  • ExecutorService

ExecutorService扩展了Executor的接口,额外具备了管理执行器和任务生命周期的方法,提交任务机制更加完善。

剑指Offer(锁)——Java线程池_第4张图片
其中的亮点在于submit方法,他其中的一个方法是接收Callable参数的,我们已经知道Callable接口解决了Runnable无法进行return的缺点。

  • ScheduledExecutorService

扩展了ExecutorService,支持Future和定期执行任务。


我们使用了不同的Executor的实现来解决不同场景的需求。

一般情况下,我们是用Executor提供了五种创建线程池的方法就足够了,但是还是有一些特殊的场景需要我们使用ThreadPoolExecutor等构造函数去创建。

下面我们来分析一下线程池的设计与实现:

下图演示了任务从提交到线程池内如何处理任务再到处理完成的引用逻辑:

剑指Offer(锁)——Java线程池_第5张图片

那么,我们也来看看ThreadPoolExecutor最有价值的的构造器的源码:

剑指Offer(锁)——Java线程池_第6张图片

下面来一一分析:

  1. corePoolSize:核心线程数量
  2. maximumPoolSize:线程不够用的时候能够创建的最大线程数
  3. workQueue:任务等待队列
  4. keepAliveTime:抢占的顺序不一定,看运气
  5. threadFactory:创建新线程,Executors.defaultThreadFactory()

此外,线程池还提供了四种策略来解决线程池的饱和问题:

  1. AbortPolicy:直接抛出异常,这是默认策略
  2. CallerRunPolicy:用调用者所在的线程来执行任务
  3. DiscardOldestPolicy:丢弃队列中靠嘴欠的任务,并且执行当前任务
  4. DiscardPolicy:直接丢弃任务
  5. 实现RejectedExecutionHandler接口的自定义handler

且在新任务提交execute之后,会执行以下的判断:

  1. 如果运行的线程少于corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的
  2. 如果线程池中的线程的数量≥corePoolSize且<maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务
  3. 如果设置的corePoolSize和maximumPoolSize相同,则创建的线程池的大小是固定的,这时候如果有新的任务提交,若workQueue未满,则将请求加入workQueue中,等待有空闲的线程去从workQueue中取任务并处理
  4. 如果运行的线程数量≥maximumPoolSize,这时如果workQueue已经满了,则通过handler锁指定的策略来处理任务

剑指Offer(锁)——Java线程池_第7张图片

线程池一共有以下几种状态:

  1. RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。
  2. SHUTDOWN:不再接受新提交的任务,但是可以处理存量任务。
  3. STOP:不再接受新提交的任务,也不处理存量任务。
  4. TIDYING:所有的任务都终止。
  5. TERMINATED:terminated()方法执行完成之后进入该状态。

状态装换图如下剑指Offer(锁)——Java线程池_第8张图片
工作线程的生命周期如下

剑指Offer(锁)——Java线程池_第9张图片

你可能感兴趣的:(剑指Offer,线程池)