揭秘java线程池-高效利器背后的精髓(中)深度剖析Executor线程池框架源码


想要在高并发的环境下编写出高效稳定的应用程序,线程池的"内功"修炼是必不可少的一课。简单的使用线程池固然可以规避资源浪费、解决线程生命周期开销问题。但要彻底发挥线程池的最大潜能,我们必须对它的核心原理有深刻的理解和把握,才能实现线程管理的精细化调度,资源利用的最大化。让我们一起揭开神秘的面纱,解开线程池的奥秘吧!

java.util.concurrent包中的ThreadPoolExecutor类,它是线程池的核心,我们先来认识一下 ThreadPoolExecutor类。


一、ThreadPoolExecutor 类概览


ThreadPoolExecutor是一个通用的线程池执行器,它提供了丰富的配置选项和灵活的线程管理策略。以下是其构造函数的签名:

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

参数说明:

  • corePoolSize:核心线程池大小。
  • maximumPoolSize:最大线程池大小。
  • keepAliveTime:非核心线程空闲存活时间。
  • unitkeepAliveTime的时间单位。
  • workQueue:一个阻塞队列,用于存放等待执行的任务。
  • threadFactory:用于创建新线程的工厂。
  • handler:当任务队列满了且线程池达到最大容量时,使用的饱和策略。

ThreadPoolExecutor的源码实现涉及到线程的创建、任务的调度、线程的生命周期管理以及线程池的关闭等多个方面。虽然线程池为我们屏蔽了诸多线程管理的细节,但要真正驾驭它,只有通过深入理解这些源码实现,才能更好地使用线程池,优化并发程序的性能。


因此我们最终还是需要深入研究一下ThreadPoolExecutor的源码实现。

下面就让我们一窥线程池的内在运作机制。

二、剖析线程池的源码实现


1、任务存储队列

在ThreadPoolExecutor中,任务的存储和排队是由一个BlockingQueue实例来管理的。BlockingQueue是Java并发包中的一个接口,它支持两个附加操作:当队列已满时,试图入队的线程会被阻塞,直到队列有空闲位置;当队列为空时,试图出队的线程也会被阻塞,直到有新元素入队。

BlockingQueue有多种不同的具体实现,比如无界队列LinkedBlockingQueue、有界队列ArrayBlockingQueue、按优先级排序的PriorityBlockingQueue等。我们可以根据实际场景的需求选择合适的队列类型,ThreadPoolExecutor则只是统一管理这个队列的读写访问。

// 任务存储队列
private final BlockingQueue<Runnable> workQueue;

// 将任务存入队列
private boolean addWorker(Runnable firstTask, boolean core) {
    // 省略其他代码...
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateAtLeast(CHECK_IF_QUEUE);
                if (rs < SHUTDOWN &&
                    (rs != QUEUE || (firstTask == null && !workQueue.isEmpty()))) {
                    if (core) {
                        // 创建核心线程
                    } else {
                        // 尝试创建非核心线程
                    }
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (!workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

上面的addWorker方法是向线程池提交任务时被调用的。在其中,线程池会先尝试将任务封装为一个Worker对象,然后再将其存入workQueue队列中。当然,如果有空闲线程,也可以直接分配给这个Worker执行。整个入队过程是通过ReentrantLock加锁实现的,以确保线程安全。


2、工作线程的生命周期

线程池中的每个工作线程都是ThreadPoolExecutor的一个内部类Worker的实例,它实现了Runnable接口。当Worker线程启动时,会不断地从workQueue取出任务执行,直到线程池被关闭。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    final Thread thread;
    Runnable firstTask;
    
    public void run() {
        try {
            Runnable task = firstTask;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);
        }
    }

    private Runnable getTask() {
        for (;;) {
            try {
                // 从阻塞队列取出任务
                return workQueue.take();
            } catch (InterruptedException retry) {
                // 中断处理
            }
        }
    }

    final void runTask(Runnable task) {
        beforeExecute(thread, task);
        try {
            task.run();
        } finally {
            afterExecute(task);
        }
    }
}

如代码所示,Worker线程的主要工作就是不断地通过getTask方法从workQueue中取出任务,并运行runTask方法执行它们。getTask使用了BlockingQueue的take方法,如果队列为空,当前线程会被阻塞等待直到有新任务可取。在执行每个任务之前,都会调用ThreadPoolExecutor的beforeExecute钩子;执行完毕后,会调用afterExecute钩子,我们可以在其中实现自定义的统计、监控、异常处理等逻辑。

特别需要注意的是,一旦Worker线程从getTask返回null, 即workQueue已被置为终止状态,该线程的run方法就会结束并退出。如果我们希望线程能继续保持存活以等待新的任务到来,就必须在合适的时机向workQueue重新注入"Terminal"任务,防止工作线程过早退出。

除此之外,我们还可以看到Worker继承自AbstractQueuedSynchronizer。这是JDK并发包中用于构建同步组件的基础框架,它为Worker提供了一些用于同步状态维护和管理的工具方法,比如占有释放等。不过这些同步状态控制对Worker的使用者来说是透明的。


3、线程回收与扩容机制

线程池在运行过程中,需要时刻关注线程的数量变化,根据任务的实际负载来调整工作线程的数目,以达到资源利用最大化。这个动态调整过程涉及到线程的扩容和回收机制。

对于核心线程和非核心线程,ThreadPoolExecutor采取了不同的处理策略:

  • 核心线程数量固定,一旦创建就不会被销毁,会一直存活在线程池中等待新任务到来。
  • 非核心线程数量则是弹性的,如果一个非核心线程在keepAliveTime时长内都一直空闲,那么它会被标记为可被回收,最终会被自动销毁。

我们来看一下核心线程如何保持存活的:

private boolean removeWorker(Worker w) {
    final ReentrantLock mainLock = this.mainLock;

    mainLock.lock();
    try {
        // 如果当前线程是核心线程,则不能移除
        if (runStateLessThan(STOP) &&
            !completedAbruptly &&
            w != null &&
            w.getState() != TERMINATED &&
            workers.remove(w)) {

            // 根据allowCoreThreadTimeOut参数来决定核心线程是否保留存活
            boolean reservedCoreThread = allowCoreThreadTimeOut ? null : this.corePoolSize;
            if (reservedCoreThread == 0)
                interruptIdleWorkers(ONLY_ONE);
            else 
                interruptIdleWorkers(EXCEPTION_KEEPER);
            
            return true;
        }
    } finally {
        mainLock.unlock();
    }
    return false;
}

这段代码中的interruptIdleWorkers方法就是用来中断空闲的工作线程,并判断它们是否可以被回收的关键所在。


4、非核心线程的回收

对于非核心线程,ThreadPoolExecutor会根据keepAliveTime参数来判断线程是否应该被回收。如果一个线程在指定的keepAliveTime时长内一直保持空闲状态,那么该线程就会被标记为将被终止(TIDYING)状态,随后会被最终回收。

private void interruptIdleWorkers(BlockingQueue<Runnable> queue) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            //如果线程存在且是空闲状态
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    //根据线程池状态和线程类型决定是否中断该线程
                    if (shouldInterruptWorker(w)) {
                        w.interruptNow();
                        interruptedWorkers.incrementAndGet();
                    }
                } finally {
                    w.unlock();
                }
            }
        }
    } finally {
        mainLock.unlock();
    }
}

private boolean shouldInterruptWorker(Worker w) {
    //如果keepAliveTime=0,非核心线程直接中断
    //否则,如果该线程空闲时间超过了keepAliveTime,也应中断
    return w.poolState() == TERMINATED ||
        (allowCoreThreadTimeOut && w.isCore())
        || (w.idleTimeout >= this.keepAliveTime);  
}

从上面的代码可以看出,ThreadPoolExecutor会遍历线程池中的所有工作线程,如果发现有非核心线程的空闲时长超过了keepAliveTime,那么就会中断并将其标记为中断状态。

当然,仅仅中断线程还不够,我们还需要一个机制去移除和回收这些多余的线程。这就是getTask方法的作用了:


private Runnable getTask() {
    boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

    for (;;) {
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            // 省略其他代码...
        } catch (InterruptedException ie) {
            // 如果发生中断,那么当前线程需要被移除回收
            wt.interrupt();
            try {
                throw new RejectedExecutionException("...");
            } catch (RejectedExecutionException ree) {
                return handlerCaughtException();
            }
        }
    }
}

在getTask中,如果当前线程是非核心线程,那么取任务时它就只能调用workQueue的poll方法而不是take。这个poll方法会阻塞至多keepAliveTime指定的时间。

一旦超时还没取到任务,poll就会返回null。这时getTask方法就会捕获一个InterruptedException中断异常,从而触发当前线程的移除回收逻辑。

这样通过keepAliveTime超时控制和getTask中断检测的双重机制,ThreadPoolExecutor就实现了自动化的非核心线程回收。


5、线程扩容机制

前面我们重点分析了线程回收的逻辑,相比之下,线程扩容的过程则相对简单一些。当有新任务进来时,如果线程池中无空闲线程,且运行线程数还未达到maximumPoolSize的上限,ThreadPoolExecutor就会创建新的工作线程来执行这个任务。

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        ...
        if (runStateAtLeast(SHUTDOWN)) {
            return false;
        }

        for (;;) {
            int c = ctl.get();
            int rs = runStateFrom(c);

            // 如果线程数量已经超过最大限制,直接返回false
            if (rs >= SHUTDOWN &&
                (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty()))
                    return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();
                if (runStateAtLeast(SHUTDOWN))
                    continue retry;
                else
                    continue;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            ...
            workerAdded = true;
        } finally {
            ...
        }

        if (workerAdded) {
            t.start();
            workerStarted = true;
        }
    }
    return workerStarted;
}

这个addWorker方法是向线程池提交新任务时被调用的关键入口。在方法内部,它会首先判断线程池是否还运行正常,以及当前线程数是否已经达到了最大上限。

如果线程数量还没有达到上限,就会通过对CAS自旋锁的操作,尝试将workerCount值+1,然后创建新的工作线程并启动它。整个过程是一个完整的CAS自旋操作,通过不断重试直到成功给workerCount完成原子递增。

值得一提的是,这里addWorker方法在创建新线程时,会区分是创建核心线程还是非核心线程。如果是核心线程,只要线程数还没到corePoolSize上限就允许创建;如果是非核心线程,只有在线程总数没超过maximumPoolSize时才会被创建。这个核心与非核心线程的区分,是线程池实现灵活伸缩的关键所在。

通过上面的线程回收与扩容机制的分析,我们可以看出ThreadPoolExecutor的线程动态调节过程是如何实现资源利用最大化的。它让核心线程一直保留,除非线程池被shutdown。


6、拒绝策略

虽然线程池通过动态调节线程数量来最大化资源利用,但极端情况下,如果任务过多而无法继续扩容时,它必须采取一些策略来处理新的任务请求。ThreadPoolExecutor使用了一个内部接口RejectedExecutionHandler来封装不同的拒绝策略。

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

ThreadPoolExecutor自带的四种拒绝策略分别是:

  1. AbortPolicy:直接抛出RejectedExecutionException异常,默认策略
  2. CallerRunsPolicy: 用调用者所在的线程来运行被拒绝的任务
  3. DiscardPolicy: 直接丢弃被拒绝的任务
  4. DiscardOldestPolicy: 丢弃队列中最早的一个未执行的任务,并尝试提交新任务

我们可以根据具体的业务场景为线程池指定合适的拒绝策略。比如对于重要的系统核心流程,可以采用CallerRunsPolicy,确保任务至少可以在当前线程中得到执行;而对于不太重要的任务,如日志记录等,我们则可以选择直接丢弃。


7、线程工厂

线程工厂ThreadFactory是另一个可定制的组件,我们可以利用它来自定义创建线程的行为。比如为线程指定有意义的名字,设置优先级等。如果不指定,线程池会使用默认的线程工厂,创建普通非守护线程。

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

ThreadPoolExecutor中也提供了一个默认的线程工厂实现DefaultThreadFactory。我们可以通过重写ThreadFactory接口的newThread方法来自定义线程创建的细节。比如下面的实现就为每个线程指定了有意义的名字:

static class CustomThreadFactory implements ThreadFactory {
    
    private AtomicInteger threadNumber = new AtomicInteger(1);
    private String prefix;

    public CustomThreadFactory(String prefix) {
        this.prefix = prefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, prefix + "-thread-" + threadNumber.getAndIncrement());
        return t;
    }
}

通过线程工厂,我们还可以给每个线程设置一些有用的上下文信息,比如客户端标识、任务类型等,供我们在后续的扩展中使用。

通过上面的分析,我们可以看到ThreadPoolExecutor中还提供了许多可扩展的点,让我们可以最大限度地定制线程池的功能。合理利用这些扩展点,可以有效地提高系统的可靠性、可维护性,满足各种复杂场景下的需求。


  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

w风雨无阻w

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

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

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

打赏作者

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

抵扣说明:

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

余额充值