线程池原理分析(ThreadPoolExecutor源码分析)

线程池的优点

  1. 线程是稀缺资源,使用线程池可以减少创建销毁线程的次数,每个工作线程都可以重复使用。
  2. 可以根据系统的承受能力,调整线程池中工作线程的数量,防止因为消耗过多内存导致服务器崩溃。

线程池的创建

创建时,有多个构造方法,参数个数不同,最终都调用下面的构造方法进行创建。

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

参数含义:

  • corePoolSize:线程池核心线程数量
  • maximumPoolSize:线程池最大线程数量
  • keepAliveTime:当活跃线程数大于核心线程数时,空闲的多余线程的最大存活时间
  • unit:存活时间的单位
  • workQueue:存放任务的队列
    常用的workQueue类型:
  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大。
  2. LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程小于核心线程数,则新建线程(核心线程)处理任务,如果当前线程数等于核心线程数,则进入队列等待调度。由于这个队列默认设置容量为Integer.MAX_VALUE,即超过核心线程数的任务都将加载到队列中,这个队列会导致maximumPoolSize参数设置失效,因为线程池中线程数永远等于corePoolSize。
  3. ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize值,则新建核心线程执行任务,如果达到了,则入队等待,如果队列已满,则新建非核心线程执行任务,如果达到了总线程数,并且队列也满了,则报错。
  4. DelayQueue:队列内元素必须实现Delayed接口,这就意味着你穿进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先入队,达到指定的延时时间才执行任务,相当于延时队列。
  • threadFactory:创建线程的方式。
  • handler:超出线程范围和队列容量的任务的处理程序
    任务拒绝策略:
    1、ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    2、ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    3、ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    4、ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

常见线程池

CachedThreadPool()

可缓存线程池,线程数无限制(Integer.MAX_VALUE),有空闲线程则复用空闲线程,若无则新建线程

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
FixedThreadPool()

定长线程池,可控制线程最大并发数,超过线程数的任务在队列中等待。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
ScheduledThreadPool()

有时间延时的线程池,这个线程池间接的利用ThreadPoolExecutor创建。支持定时及周期性任务执行。任务队列使用的是延时队列,可以规定时间延迟。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
SingleThreadExecutor()

单线程线程池,核心线程和最大线程数都为1,所有任务按指定顺序执行,遵循队列的入队出队规则。

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
SingleThreadScheduledExecutor()

单线程可延时线程池,该线程池结合了ScheduledThreadPool和SingleThreadExecutor两个线程池的内容。

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

线程池的实现原理

提交一个任务到线程池中,线程池的处理流程如下:

1、判断线程池中的核心线程是否都在执行任务,如果没有则创建一个新的工作线程来执行任务。如果核心线程数已经达到创建限制,并且核心线程都在执行任务,则进行下个流程。

2、线程池判断任务队列是否已满,如果任务队列未满,则将任务提交存储到任务队列中。如果任务队列满了,则进入下个流程。

3、判断线程池中的线程是否都处于工作状态,如果没有,则创建一个工作线程(非核心线程)来执行任务,如果这时,线程都在工作且线程数达到线程池最大线程数量,则交给饱和策略来处理这个任务(handler)。

在这里插入图片描述

源码分析

下面写一个简单例程进行源码分析的入口,测试demo如下:

public class ThreadPoolTest {
    public static void main(String[] args) {
        LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue);
        for (int i = 0; i < 16; i++) {
            poolExecutor.execute(()->{
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println("线程池中活跃的线程数:"+ poolExecutor.getPoolSize());
            if (queue.size() > 0){
                System.out.println("-------------------任务队列阻塞的线程数"+queue.size());
            }
        }
        poolExecutor.shutdown();
    }
}
  • 首先查看线程池ThreadPoolExecutor类的execute方法,可以看到注释内容即是线程池实现原理。具体几步解释见下面中文解释。
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
    	//判断线程数量是否小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
            //创建线程并启动
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
    	//向任务队列中提交任务
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //提交成功后,进行校验,判断线程池是否运作中,如果线程池没有运行中,则移除插入的任务,然后提交给饱和策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //判断可用线程数是否等于0,是,开启工作线程执行任务
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
    //尝试开启工作线程,如果失败,则提交给饱和策略
        else if (!addWorker(command, false))
            reject(command);
    }
  • 接下来,我们查看addWorker方法,创建线程的方法。该方法有两个参数,而第二个core参数,表示的创建的是核心线程还是非核心线程,传参为boolean型。
    1. 如下图第一个框内,是进行判断,判断线程池状态大于等于shutdown(即状态不是运行中),并且再满足后面的条件,则不处理直接返回。
    2. 第二个框中,是更新线程池线程数量,根据传入的core 参数,判断是否为核心线程,如果为true,且线程数小于corePoolSize,则跳出循环,创建核心线程。

在这里插入图片描述

​ 3.接下来的代码如下图,第一个框中,将任务封装到Worker类中,并获得线程池主锁,保证线程安全。在这里,线程池中的线程是通过Worker类来实现的。

​ 4.第二个框中,再次校验线程池状态和刚创建的线程状态,接下来将线程加入线程池中(workes)。

​ 5.第三个框中,启动刚创建的线程。

在这里插入图片描述

  • 下面我们查看Worker类的run方法,run方法中调用了runWorker方法,源码如下图。
    1. 第一个框中,当该线程是刚创建第一次执行任务,或者从任务队列中获取到任务,首先判断线程池状态,如果线程池不是停止状态,清除当前线程的中断标志后再次判断线程池状态;如果前者判断为true,接下来再次判断当前线程是否为中断,再结合前者条件,true:将当前线程中断,false:执行当前线程。
    2. 第二个框中,执行任务开始前操作钩子。
    3. 第三个框中,执行任务。
    4. finally块中,执行执行任务后钩子。

在这里插入图片描述
在上述简单demo中,运行后会出现如下错误,因为我们设置的LinkedBlockingQueue队列的容量是5,核心线程为5,最大线程数为10,for循环将开启总共16个线程执行16次任务。当线程池中线程数达到最大线程数10后,开始往队列中存储任务,存储5个,队满后,线程数和队列容量都达到了临界点,所以执行饱和策略,由于未定义,所以执行默认的RejectedExecutionException异常。

线程池中活跃的线程数:7
-------------------任务队列阻塞的线程数5
线程池中活跃的线程数:8
-------------------任务队列阻塞的线程数5
线程池中活跃的线程数:9
-------------------任务队列阻塞的线程数5
线程池中活跃的线程数:10
-------------------任务队列阻塞的线程数5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at threadPools.ThreadPoolTest.main(ThreadPoolTest.java:19)

接下来,我们定义一个丢弃策略,在创建线程池时将该参数传入,再次执行将不会出现上述异常,因为丢弃策略是将超出限制的线程丢弃不执行。除此之外,还可以自定义其他饱和策略来处理当线程池达到饱和状态的情况。

public class ThreadPoolTest {
    public static void main(String[] args) {
        LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
        //丢弃策略,多余的丢弃,不报错
        ThreadPoolExecutor.DiscardPolicy handler = new ThreadPoolExecutor.DiscardPolicy();
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue,handler);
        for (int i = 0; i < 16; i++) {
            poolExecutor.execute(()->{
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println("线程池中活跃的线程数:"+ poolExecutor.getPoolSize());
            if (queue.size() > 0){
                System.out.println("-------------------任务队列阻塞的线程数"+queue.size());
            }
        }
        poolExecutor.shutdown();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值