【并发编程】(二十)线程池的实现原理简述

1.线程池概述

1.1.什么是线程池

我们平时使用到线程的时候会去new Thread(),这种方式会创建一个线程,然后使用完成后被回收掉。不断的创建和销毁对服务器的资源也是一种浪费,针对这种资源浪费的情况,就出现了线程池,我们想要使用线程的时候,从线程池中去取出来,用完之后还回去,起到线程复用的效果。

所以,什么是线程池呢?
线程池就是一个用来存放线程对象的容器,这个容器提供了线程的调度功能。

1.2.线程池的作用

正是因为使用线程池有额外的好处,我们在日常开发的规范中,是规定使用线程池来替代直接new Thread()这种创建线程的方式,使用线程池有以下几个好处:

  • 可以重复使用线程,减少线程创建和销毁的资源开销。
  • 可以限定线程的创建数量,避免创建过多的线程导致系统资源耗尽。同时,线程数量过多也会导致CPU的时间片切换更加频繁。
  • 除了以上两点指点,线程池还封装了一些线程调度的功能,比如定时任务的线程池。

2.线程池的使用

2.1.线程池的创建方式

线程池可以通过一个工厂类Executors来创建,常见的创建方式有下面几种:

  • Executors.newFixedThreadPool(100):创建一个核心线程数量为100的定长线程池,如果核心线程全部处于忙碌状态时,新的任务就会在一个无界队列中排队,所以这种方式有可能导致系统资源耗尽。
  • Executors.newCachedThreadPool():创建一个可缓存的线程池,这个线程池会不断的创建线程,当线程空闲时又会被回收掉,这种方式也可能导致系统资源耗尽。
  • Executors.newSingleThreadExecutor():创建一个单例线程池,这个线程池中只有1个线程。
  • Excutors.newScheduledThreadPool(10):创建一个可以定时执行任务的线程池。

除了使用工厂类来创建线程池外,我们还可以手动的去创建线程池,手动创建的方式可以更加灵活的配置参数,避免资源耗尽的风险。上面的4中创建线程池的方法,最终都会调用到同一个方法:new ThreadPoolExecutor();
至于他们为什么会存在功能上的区别,我们可以先看一看,线程池有哪些参数。

2.2.创建线程池的参数

ThreadPoolExecutor的构造方法有多个重载,下面我截了一张参数最全的重载方法:
在这里插入图片描述
然后分别解释一下各个参数的含义:

  • corePoolSize:核心线程数,创建后就会一直存在于线程池中,即使是处于空闲状态也不会被回收,如果将allowCoreThreadTimeOut 设置为true,则核心线程也会被回收 。
  • maximumPoolSize:最大线程数,定义线程池中最多可以创建多少个线程,非核心线程数=最大线程数-核心线程数,非核心线程会在空闲一定时间后被回收掉。
  • keepAliveTime:非核心线程在空闲是的存活时间,空闲时间超过这个时间会被回收。
  • unit:存活时间的单位。
  • workQueue:工作队列,核心线程都处于忙碌状态时,新的任务会进入到工作队列中排队,工作队列是使用阻塞队列实现的。
  • threadFactory:线程工厂,用于创建线程池中的工作线程,一般会通过这个工厂定义一个线程名创建的模板,为每个线程赋予一个有意义的线程名,方便调试。
  • handler:拒绝策略,如果最大线程数和工作队列都满了就需要执行拒绝策略。

拒绝策略有以下几个实现:
在这里插入图片描述

  • AbortPolicy: 直接抛出异常。
  • CallerRunsPolicy:使用调用者的线程直接执行任务(这种方式异步变同步了)。
  • DiscardPolicy:丢掉新进入的任务。
  • DiscardOldestPolicy:丢掉队列中最先进入队列的任务。

2.3.线程池的使用

结合上面所说的,我们通过完整的参数创建一个线程池:

private static AtomicLong threadCount = new AtomicLong(0);

public static ExecutorService testCreate() {
    // 线程工厂
    ThreadFactory threadFactory = r -> Executors.defaultThreadFactory()
                .newThread(new Thread(r, String.format("test-pool-%d", threadCount.getAndIncrement())));
    return new ThreadPoolExecutor(10,
            100,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            threadFactory,
            new ThreadPoolExecutor.AbortPolicy());
}

这样我们就得到了一个线程池,接下来试一下调用结果,写一个main

public static void main(String[] args) throws InterruptedException {
    ExecutorService service = testCreate();
    service.execute(() -> System.out.println(Thread.currentThread().getName()));
    service.submit(() -> System.out.println(Thread.currentThread().getName()));

    service.shutdown();
    TimeUnit.SECONDS.sleep(5);
}

打印结果如下:

test-pool-0
test-pool-1

2.4.ExecutorService

ExecutorService及其父接口Executor定义了线程池和线程的调度方法:
在这里插入图片描述

  • shutdown()是用来关闭线程池的方法。
  • submitinvokeAll/invokeAny都是用来执行线程任务的,同时在父接口中还有一个execute方法。

他们的主要区别在于,submit/invoke使用了FutureTask,可以阻塞主线程的运行,等待结果返回。详情可以查看上一篇博客《(十九)Callable的实现原理简述》

3.线程池的实现原理

3.1.线程池的执行流程

通过上面的概述和使用两个点,我们可以先总结一下线程池的流程。
在这里插入图片描述
再看看源码中是怎么描述的,我们可以通过executorService.execute()找到方法入口:

public void execute(Runnable command) {
    if (command == null) throw new NullPointerException();
    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);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 判断线程池总数是否已满
    else if (!addWorker(command, false))
    	// 触发拒绝策略
        reject(command);
}

判断新增的线程是不是核心线程的关键就在addWorker的第二个参数,true就是判断核心线程数,false就是判断总线程数,源码如下:
在这里插入图片描述

3.2.工作线程的创建

无论使用的Runnable还是Callable,最终都需要一个Thread对象来启动线程,然后回调它们的run()/call()方法,上面提到了一个addWorker方法,方法里面会创建一个Worker对象,这个对象就是线程池的工作线程对象,它实现了Runnable接口:
在这里插入图片描述
在经过线程池中线程的数量判断后,如果当前的线程数量小于核心线程数(或者总线程数),就会去创建工作线程,下面看一下Worker的构造方法:

Worker(Runnable firstTask) {
    setState(-1);
    // firstTask就是调用excute时传入的Runnable对象,也就是待执行的任务。
    this.firstTask = firstTask;
    // 把Worker自己传进去,创建工作线程。
    this.thread = getThreadFactory().newThread(this);
}

到此工作线程就创建好了,在addWorker()方法的最后,就会调用start()启动线程,至此同步流程就完成了,接下来只需要关注Worker是如何执行异步任务的。

3.3.工作线程的运行

我们知道线程在启动之后,会等待操作系统调度,回调run()方法,所以我们现在直接去找Worker中的run()方法即可。
在这里插入图片描述
在上面的Worker构造方法中可以看到,初次创建的工作线程会保存待执行的任务对象(Runnable对象),所以工作线程在第一次调度的时候会直接使用这个任务对象,然后在复用线程的时候才会从工作队列中去获取。
下面的源码省略了部分不重要的代码:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // 初次调度,直接使用创建时传入的任务对象
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); 
    boolean completedAbruptly = true;
    try {
    	// 复用时使用getTask()从工作队列中获取
        while (task != null || (task = getTask()) != null) {
            w.lock();
            try {
                beforeExecute(wt, task);
                // 执行Runnable的run()方法
                task.run();
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

从阻塞队列中获取任务,如果队列中已经没有正在等待的任务了,就会将自己放入到条件等待队列中,等待下一个任务进入队列的时候,再被其它线程唤醒继续执行任务。
阻塞队列的阻塞唤醒功能就是使用ReentrantLock中的Condition实现的,详情可以去了解《(十五)Condition的使用及其阻塞唤醒原理》

3.4.工作线程的回收

严格意义上讲,线程池中的工作线程没有核心线程和非核心线程的明确区分,只要是线程的数量大于了核心线程数并且空闲时间超过了存活时间后,就会依次将线程回收直到线程数量与核心线程数量相等。

对于线程的回收,就是线程停止运行,而在线程池中为了让工作线程复用,在工作线程的run()中写入了一个循环,只需要将这个循环打破,让run()方法执行完毕,线程自然就被回收了。

如何将循环打破呢?
分两种情况,异常停止和正常停止。

  • 异常停止:使用interrupt中断线程的运行。
  • 正常停止:满足循环终止的条件即可。

我们更多的时候是通过正常停止的方式让线程池回收线程的,那我们只需要关注while循环的两个条件:

while (task != null || (task = getTask()) != null) 

在一次任务执行完毕后,task就会被置为null,所以我们重点看第二个条件,只要getTask()返回null即可,我们进入这个方法:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挥之以墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值