JUC学习笔记四:ThreadPool线程池

ThreadPool线程池

线程池的优势:

在多核cpu没有普及的年代,单核cpu就像是假的多线程。一个cpu需要在多个线程之间来回切换。现在的多核电脑,多个线程可以各自跑在独立的CPU上,不用切换,效率高。

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用;控制最大并发数;管理线程。

  • 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
  • 第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  • 第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的三大方法 :

首先,我们要先明确和线程池有关的类和接口的关系,如下图:
在这里插入图片描述

可以发现,我们经常拿来引用的ExecutorService是一个接口,它也继承了Executor接口,Executor接口中只定义了void execute(Runnable command);一个方法,ExecutorService则多定义了线程池的几个操作方法,如shutdown()等。

在这之前,我一直使用的创建线程池的方法是,调用Executors工具类里面封装好的创建特定线程池的静态方法,包括:

        ExecutorService threadPool = Executors.newFixedThreadPool(5);//一个池子里面5个工作线程
        ExecutorService threadPool2 = Executors.newSingleThreadExecutor();//相当于一个池子仅有1个工作线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();//根据需要来创建线程,可扩容可收缩

这里的三个方法,之前笔记里面已有介绍,不在赘述。
点进去这三个静态方法的源码,我们发现他们分别是这样的:

	//1.newFixedThreadPool
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    //2.newSingleThreadExecutor
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    //3.newCachedThreadPool
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

我们发现,这三个Executors工具类中的创建线程池的方法,最后实际上都创建出来了一个 【ThreadPoolExecutor】 只是构造器内部的参数不同而已。
我们打开阿里巴巴开发手册,看一下开发当中,用哪一种方法创建线程池最好。结果如下:

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
(此段内容来自阿里巴巴java开发手册1.4.0)

很明显,得到的答案是用更加手动化的ThreadPoolExecutor来根据业务需要定制创建线程池。

线程池的七大重要参数:

确定了创建线程池要用的方法之后,就要去看如何用new ThreadPoolExecutor的方法创建线程池,打开ThreadPoolExecutor的构造器(其中有很多重载的方法,不管哪个方法前五个参数必须手动设置):

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

这七个参数分别是:

  • corePoolSize:核心池的大小
  • maximumPoolSize:最大池的大小
  • keepAliveTime:存活时间
  • unit:时间单位
  • workQueue:工作的阻塞队列
  • threadFactory:创建线程的工厂类(一般使用默认即可)
  • handler:拒绝策略

当设定好这几个参数,线程池会怎样运行呢?
首先先来看线程池的工作流程:
在这里插入图片描述
解释:
1.首先,假设和图中一样,corePoolSize = 2,maximumPoolSize= 5,workQueue为容量等于3的阻塞队列。第1步,来了两个线程需要执行,线程池会调用核心池里面的两个线程来执行。

2.核心池里面的线程正在执行,此时又来了三个需要执行的线程,【此时,线程池并不会直接将扩容,而是先将这些线程放入阻塞队列中】,此时一共有五个线程。两个在核心池运行当中,三个在阻塞队列中等待。

3.此时,假设又来了3个需要执行的线程。【此时,阻塞队列已经满了,但是线程池内还没有达到最大线程数,这个时候线程池会才会将容量扩容到5,以便在阻塞队列中等待已久的线程执行】注意:5已经是线程池的最大容量。新来的3个线程进入了空出来的阻塞队列中等待。这个时候整个程序中有8个线程,2个在核心池执行,3个在扩展出来的池子执行,还有三个在阻塞队列中等待,线程池已经不能接受任何一个新的线程的执行请求。

4.此时线程池已经“满载”,连阻塞队列的等待线程也满了,如果这时又来了若干个线程需要执行,那么,线程池就会执行拒绝策略(拒绝策略根据业务的需要分为4种)。

线程池的执行流程:

可以看出,线程池中,线程的处理流程如下:
在这里插入图片描述
以下的解释说明了,线程池的工作完整机制。
1、在创建了线程池后,开始等待请求。

2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

  • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
  • 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

自定义线程池

线程池的拒绝策略

  • 什么是拒绝策略?什么时候用到?
    线程池中正在运行的线程数量已经达到maximumPoolSize最大线程容量,同时连阻塞队列都被占满了。这时候,线程池就要执行拒绝策略来拒绝新的线程进入。
  • 四种拒绝策略这里做一个测试:
public class ThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
        		2,//核心池大小
                5,//最大池大小
                2L,
                TimeUnit.SECONDS,//与上个参数组合为收缩等待时间
                new LinkedBlockingQueue<>(3),//长度为3的阻塞队列
                Executors.defaultThreadFactory(),//默认线程工厂
                new ThreadPoolExecutor.AbortPolicy()//分别测试:替换为4种拒绝策略
                );

        try {
            //模拟十个线程进来,池子里面最多跑5个线程,阻塞队列容量为3,尝试跑9个线程看看四种拒绝策略如何拒绝
            for (int i = 1; i <= 9; i++) {
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName()+"  办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

运行结果:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
8
pool-1-thread-1  办理业务
pool-1-thread-2  办理业务
pool-1-thread-3  办理业务
pool-1-thread-4  办理业务
pool-1-thread-1  办理业务
pool-1-thread-2  办理业务
pool-1-thread-5  办理业务
pool-1-thread-4  办理业务
java.util.concurrent.RejectedExecutionException

2…CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。这里是main主线程启动了线程池,所以多余的一个线程交给了main线程执行。

pool-1-thread-1  办理业务
pool-1-thread-3  办理业务
pool-1-thread-2  办理业务
pool-1-thread-3  办理业务
pool-1-thread-1  办理业务
main  办理业务
pool-1-thread-5  办理业务
pool-1-thread-4  办理业务
pool-1-thread-2  办理业务

3.DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。
如果允许任务丢失,这是最好的一种策略。

pool-1-thread-1  办理业务
pool-1-thread-2  办理业务
pool-1-thread-3  办理业务
pool-1-thread-2  办理业务
pool-1-thread-1  办理业务
pool-1-thread-5  办理业务
pool-1-thread-4  办理业务
pool-1-thread-3  办理业务

4.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。

pool-1-thread-1  办理业务
pool-1-thread-2  办理业务
pool-1-thread-1  办理业务
pool-1-thread-2  办理业务
pool-1-thread-3  办理业务
pool-1-thread-4  办理业务
pool-1-thread-1  办理业务
pool-1-thread-5  办理业务

具体用哪一种拒绝策略,由业务需要决定。
实际工作中,不能用三大方法建立线程池!当然,手写小的demo建立多线程时可以使用无妨。

合理设置最大线程数

tips:如何设置最大线程数最合理?

  • CPU密集型:线程个数为CPU核数或者多一点点。这几个线程可以并行执行,不存在线程切换到开销,提高了cpu的利用率的同时也减少了切换线程导致的性能损耗 。

  • IO密集型:线程个数为CPU核数的两倍。其中一半的线程在IO操作的时候,另一半线程可以继续用cpu做其他事情,提高了cpu的利用率 。

发布了16 篇原创文章 · 获赞 2 · 访问量 449

猜你喜欢

转载自blog.csdn.net/qq_31314141/article/details/104212514
今日推荐