线程池详解

为什么要引入线程池?

在未引入线程池之前,我们完成一个任务,需要经过自己创建线程,执行任务,销毁线程这三个步骤。

这存在什么问题呢?

1)浪费资源:线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些系统资源。频繁地创建和销毁线程会浪费大量的系统资源。

2)花费时间变长:线程的创建和销毁都需要时间,当数量太大的时候,会影响效率。

3)线程的不合理分布:在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程自身无法解决的。

4)无法功能拓展

因此,引入线程池来协调这些线程、拓展功能。

线程池有以下几个作用:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。

  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。

  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。

  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

了解了为什么要引入线程池之后,我们先来看看线程池要怎么用,再来分析线程池的底层原理。

线程池怎么用?

我们引入线程池,就是为了让线程池去创建线程,然后执行任务。代替我们原先自己手动创建线程。

public class ExecutorCase {
​
    public static void main(String[] args) {
      // 创建线程池
      Executor executor = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            // 将任务放进线程池里,在使用者眼里,相当于任务开始执行了。至于线程池是如何用自己创建的线程执行任务的,就是底层的东西了
            executor.execute(new Task());
        }
    }
}
​
static class Task implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

由上例可以看到,线程池是通过Executors类调用其静态方法创建的。使用Executors类创建出线程池我们称之为内置线程池

Executors工具类可以创建多种类型的线程池:

  • Executors.newFixedThreadPool:创建固定线程数量的线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
​
​
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
  • Executors.newSingleThreadExecutor:创建只有一个线程的线程池。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
​
​
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}
  • Executors.newCachedThreadPool:创建可根据实际情况调整线程数量的线程池。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
​
​
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}
  • Executors.newScheduledThreadPool:创建给定的延迟后运行任务或者定期执行任务的线程池。

仔细观察上面几种在Executors类中已经写好的线程池,可以发现它们方法中基于ThreadPoolExecutor类创建线程池。不同点在于传入ThreadPoolExecutor构造器参数不同。可以猜测,线程池底层如何创建线程,如何协调线程,执行任务等等机制都与这些参数有关。我们只需要弄懂这些参数背后的具体含义,再根据这些具体线程池所传的具体参数是什么,大体就可以知道这个线程池的底层是如何运作的了。

ThreadPoolExecutor类

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler
                           ) {
    // 第一处
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    
    // 第二处:队列、线程工厂、拒绝处理服务都必须有实例对象。
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    
    // 其他代码····
}

下面我们来分析ThreadPoolExecutor构造器里的参数:

  • corePoolSize:表示常驻核心线程数。如果等于0 ,则任务执行完之后没有任何请求进入时销毁线程池的线程,如果大于0 ,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。

  • maximumPoolSize:表示线程池能够容纳同时执行的最大线程数。

  • keepAliveTime:示线程池中的线程空闲时间。当空闲时间达到 keepAliveTime 值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止。也就是说核心线程外的线程才会被销毁。

  • TimeUnit:表示时间单位。 keepAliveTime 的时间单位通常是TimeUnit.SECONDS。

  • workQueue:表示缓存队列。当请求的线程数大于 maximumPoolSize 线程进入 BlockingQueue 阻塞队列。阻塞队列有以下类型:

    • 1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务。

    • 2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene。

    • 3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene。

    • 4、priorityBlockingQuene:具有优先级的无界阻塞队列。

  • threadFactory:表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个 factory 增加组名前缀来实现的。

  • handler:表示执行拒绝策略的对象。当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

    • 1、AbortPolicy:直接抛出异常,默认策略。

    • 2、CallerRunsPolicy:直接在调用execute方法的线程中运行,调用任务的 run() 方法。

    • 3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务。

    • 4、DiscardPolicy:直接丢弃任务。

使用ThreadPoolExecutor构造函数创建线程

相比于前面介绍的使用Executors工具类创建线程池,看了这些内置线程池的源码,发现都是通过ThreadPoolExecutor构造函数创建的。于是我们可以自己直接使用ThreadPoolExecutor构造函数创建线程。传入ThreadPoolExecutor构造函数的参数由我们自己定义。

public class UserThreadPool {
    public static void main(String[] args) {
        // 缓存队列设置固定长度为2,为了快速触发rejectHandler
        BlockingQueue queue = new LinkedBlockingQueue(2);
​
        // 假设外部任务线程的来源由机房1和机房2的混合调用
        UserThreadFactory f1 = new UserThreadFactory("第1机房");
        UserThreadFactory f2 = = new UserThreadFactory("第2机房");
​
        UserRejectHandler handler = new UserRejectHandler();
​
        // 核心线程为1,最大线程为2,为了保证触发rejectHandler
        ThreadPoolExecutor threadPoolFirst = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue, f1, handler);
​
        // 利用第二个线程工厂实例创建第二个线程池
        ThreadPoolExecutor threadPoolSecond = new ThreadPoolExecutor(1, 2, 60, TimeUnit.SECONDS, queue, f2, handler);
​
        // 创建400个任务线程
        Runnable task = new Task();
        for (int i = 0; i < 200; i++) {
            threadPoolFirst.execute(task);
            threadPoolSecond.execute(task);
        }
    }
}
​
// 任务执行体
class Task implements Runnable {
    private final AtomicLong count = new AtomicLong(0 L);
​
    @Override
    public void run() {
        System.out.println("running " + count.getAndIncrement());
    }
​
}
​
public class UserThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);
​
    // 定义线程组名称,在使用jstack来排查线程问题时,非常有帮助
    UserThreadFactory(String whatFeatureOfGroup) {
        namePrefix = "UserThreadFactory's " + whatFeatureOfGroup + "-Worker-";
    }
​
    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();
        Thread thread = new Thread(null, task, name, 0, false);
        System.out.println(thread.getName());
​
        return thread;
    }
}

从execute源码开始分析

从任务的角度分析

任务提交
Executor.execute()

任务执行完没有返回值。从方法中参数类型可以看出使用这种方式提交的任务必须实现Runnable接口。

ExecutorService.submit()

任务执行完有返回值。

任务调度

进入execute方法,分析任务是怎么被线程池处理的。

检查现在线程池的运行状态(是否为RUNNING)、运行线程数(workerCount )、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

由以上过程可以分析出,任务被提交给线程池后,有三种命运:一种情况是任务直接由新创建的线程执行;一种情况是任务被放到阻塞队列里等待空闲线程来申请并执行;一种情况是任务直接被拒绝了。

任务缓冲

线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

任务申请

getTask()方法

整个getTask操作在自旋下完成: 1、workQueue.take:如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行; 2、workQueue.poll:如果在keepAliveTime时间内,阻塞队列还是没有任务,则返回null;

所以,线程池中实现的线程可以一直执行由用户提交的任务。

任务拒绝

在execute方法中,如果判断出要拒绝这个任务,那么就会执行reject方法。

 拒绝策略的那些类实现这个接口,然后重写rejectedExecution方法。这样我们的handler类型就是选择的拒绝策略某个类的类型,调的rejectedExecution方法自然就是重写的方法。

从线程的角度分析

在源码里,Worker类代表线程。

线程增加

addWorker(Runnable firstTask, boolean core);

  • firstTask:指定新增的线程执行的第一个任务,可以为空。

  • core:true:表示在新增线程时会判断当前活动线程数是否<corePoolSize,false:表示新增线程前需要判断当前活动线程数是否<maximumPoolSize。

addWorker执行流程:

1、判断线程池状态,如果线程池状态还未停止,那么执行(2)

2、判断此时线程池里的线程数,如果小于core,则跳出循环,开始创建新的线程。具体实现如下:

3、创建一个Woker对象,表示一个新线程。用ReentrantLock加锁,使得这个线程安全添加进HashSet里。

Woker类:

  1. 继承了AQS类。

  2. 实现了Runnable接口,可以将自身作为一个任务在工作线程中执行;

  3. 当前提交的任务firstTask作为参数传入Worker的构造方法;

  4. 有thread属性。当我们new一个worker的时候,相当于底层帮我们用线程工厂创建了一个线程。

线程执行任务

addWorker方法里调用 t.start(); 底层执行的就是Worker里的run方法。run方法里再调用runWorker方法。

1、获取第一个任务firstTask,执行该任务。执行任务前会加锁,执行完后释放锁。

2、该线程将第一个执行完后,就循环去阻塞队列里获取任务执行,即getTask方法。如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源。

线程回收

Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

参考

《码出高效:Java开发手册》

Java线程池实现原理及其在美团业务中的实践 - 美团技术团队 (meituan.com)

深入分析java线程池的实现原理 - 简书 (jianshu.com)

Java并发常见面试题总结(下) | JavaGuide

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值