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

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

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

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

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

Fork/Join框架

Java7提供的一个并行处理任务的框架,将大任务切分成多个小任务,最终汇总每个小任务结果得到大任务结果的框架。

可以采用递归的方法拆成一个个的小任务分治去解决。从原理上来说和MapReduce一样。

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

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

被窃取的任务的队列从头部获取执行,窃取任务的队列从尾部获取执行。
在这里插入图片描述
为什么使用线程池???

  1. 重复使用线程,降低资源消耗;
  2. 提高线程的可管理性,提高工程的可管理性。

下面来介绍一下Executor框架:

Executor框架是一组执行策略调用,资源调度和控制的异步任务框架,目的是将任务提交和任务如何运行分离开的机制。
在这里插入图片描述
JUC提供了三个Executor接口

  1. Executor

作用是创建一个新线程并且立刻使用,也可能是使用已有的运行线程去运行当前的任务,也有可能是看设置线程池的容量或者阻塞队列的容量来决定是够将传入的线程放入阻塞队列中或者拒绝接受传入的线程。
在这里插入图片描述
之前创建线程是这样的:

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

使用Executor之后就会变成这样:

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

ExecutorService扩展了Executor接口,额外具备了管理执行器和任务生命周期的方法,提交任务机制更加完善。
在这里插入图片描述
其中的亮点在于submit方法其中的一个方法是接收Callable参数的,已经知道Callable接口解决了Runnable无法进行return的缺点。
3. ScheduledExecutorService

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

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

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

接下来来分析一下线程池的设计和实现:

下图是任务从提交到线程池内如何处理任务再到处理完成的引用逻辑:
在这里插入图片描述
来看一下ThreadPoolExecutor的构造器的源码:
在这里插入图片描述
对传入的参数进行简单的介绍:

  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锁指定的策略来处理任务。
    在这里插入图片描述

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

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

状态转换图
在这里插入图片描述
工作线程的生命周期
在这里插入图片描述

线程池的大小如何选定

  • CPU密集型:线程数 = 按照核数或者核数 + 1设定;
  • IO密集型:线程数 = CPU核数 * (1 + 平均等待时间 / 平均工作时间)
发布了242 篇原创文章 · 获赞 23 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_44240370/article/details/104123103