java线程池详解

来源:javaguide 部分做了精简,部分做了进一步的解释。 持续更新

什么是线程池?

线程池是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程不会被立即销毁,而是等待下一个任务。

为什么要用线程池?

1、降低资源消耗 :线程池里的线程是可以复用的,线程完成任务之后不会立即销毁,而是回到线程池中等待下一个任务,这避免了频繁创建和销毁线程带来的开销。 2、提高响应速度 :任务到达之后,可以直接交给线程池中已存在的空闲线程去执行,省去了创建线程的时间,任务能够更快得到处理。 3、提高线程的可管理性:线程池允许我们统一管理线程池中的线程,我们可以配置线程池的核心线程数、最大线程数、任务队列的类型和大小、拒绝策略等。这样就能控制线程的总量,防止资源耗尽,保证系统稳定性。同事,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。

如何创建线程池?

方式一:通过ThreadPoolExcutor构造函数创建(推荐) 这是最推荐的方式,因为它允许开发者明确指定线程池核心参数,对线程池的运行行为有更精细的控制,从而避免资源耗尽的风险。

java 复制代码
    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();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

方式二:通过Executors工具类创建(不推荐用于生产环境) 点进Executors类里面,ctrl + F12即可查看这个类的所有方法。 这个类可以快捷地帮助我们创建一些种类的线程池,比如:

  • FixedThreadPool:固定线程数量的线程池。当有一个新任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在任务队列中,等到有线程空闲时便处理在任务队列中的任务。
  • SingleThreadExecutor:只有一个线程的线程池。当有一个新任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在任务队列中,等到有线程空闲时便处理在任务队列中的任务。
  • CachedThreadPool:根据实际情况调整的、核心线程数为0的线程池,当有新的任务到来时,有空闲线程可用,则会使用空闲线程;若所有线程都在工作,则会创建新的线程处理任务。所有线程在执行完任务之后,将返回线程池以供复用,在空闲了一段时间之后会被销毁。 来看一下CachedThreadPool的构造函数:
java 复制代码
 public static ExecutorService newCachedThreadPool() {
   return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                 60L, TimeUnit.SECONDS,
                                 new SynchronousQueue<Runnable>());
}

在这个线程池中,核心线程数为0,最大线程数设置为 Integer.MAX_VALUE,表示没有核心线程,非核心线程是无界的。keepAliveTime 为 60 秒,空闲线程等待新任务的最长时间是 60 秒。使用了阻塞队列 SynchronousQueue,这是一个不存储元素的阻塞队列,每一个插入操作必须等待另一个线程的移除操作,同理一个移除操作也得等待另一个线程的插入操作完成。

  • ScheduledThreadPool:用于执行延迟任务或者周期性任务的线程池

为什么不推荐使用Executors?

通过ThreadPoolExecutor构造函数的创建方式,能够让使用者更加明确线程池的运行规则,规避资源耗尽的风险。 使用Executors的弊端如下:

  • FixedThreadPoolSingleThreadExecutor使用的阻塞队列是LinkedBlockingQueue,任务队列最大长度为Integer.MAX_VALUE,可以看作是无界的,可能堆积大量的任务,从而导致OOM
  • CachedThreadPool使用的是同步阻塞队列SynchronousQueue,允许创建的线程数是Integer.MAX_VALUE,如果任务过多且执行速度较慢,可能会创建大量线程,从而导致OOM。
  • ScheduledThreadPoolSingleThreadScheduledExecutor使用的无界延迟阻塞队列DelayWorkQueue,任务队列最大长度为Integer.MAX_VALUE,可以看作是无界的,可能堆积大量的任务,从而导致OOM。 源码如下:
java 复制代码
public static ExecutorService newFixedThreadPool(int nThreads) {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
    return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());

}

public static ExecutorService newSingleThreadExecutor() {
    // LinkedBlockingQueue 的默认长度为 Integer.MAX_VALUE,可以看作是无界的
    return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));

}

// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE`
public static ExecutorService newCachedThreadPool() {

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

}

// DelayedWorkQueue(延迟阻塞队列)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

线程池常见参数有哪些?

构造函数如下:

java 复制代码
    /**
     * 用给定的初始参数创建一个新的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();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize: 核心线程数。任务队列未达到最大容量时,可以同时运行的最大线程数量。
  • maximumPoolSize:最大线程数。任务队列达到最大容量时,可以同时运行的最大线程数量。
  • workQueue:任务队列。新任务被提交到线程池时,会判断当前运行的线程数量是否达到核心线程数,如果已经达到核心线程数,并且任务队列没有满,新任务就会被存放在队列中。
  • keepAliveTime:线程空闲后的存活时间。当线程池中的线程数量大于核心线程数,即有非核心线程时,这些非核心线程空闲后不会被立即销毁,而是经过keepAliveTime的空闲时间之后才会被销毁。
  • unit:keepAliveTime的时间单位。
  • threadFactory:线程工厂。创建新线程的时候会按照线程工厂指定的规则创建出来,如线程名字的前缀等。
  • handler:拒绝策略。当任务队列的数量达到上限,线程数也达到maximumPoolSize时,线程池对于新提交的任务的处理策略。

线程池的核心线程会被回收(销毁)吗?

ThreadPoolExcutor默认即使核心线程空闲了,也不会回收它们。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。如果线程池是被用于周期性使用的场景(比如定时任务),且频率不高,可以考虑将 allowCoreThreadTimeOut(boolean value) 方法的参数设置为true,这样就会回收空闲的核心线程了。线程空闲后的存活时间用keepAliveTime执行。 源码:

java 复制代码
public void allowCoreThreadTimeOut(boolean value) {
    // 核心线程的 keepAliveTime 必须大于 0 才能启用超时机制
    if (value && keepAliveTime <= 0) {
        throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
    }
    // 设置 allowCoreThreadTimeOut 的值
    if (value != allowCoreThreadTimeOut) {
        allowCoreThreadTimeOut = value;
        // 如果启用了超时机制,清理所有空闲的线程,包括核心线程
        if (value) {
            interruptIdleWorkers();
        }
    }
}

核心线程空闲时处于什么状态?

核心线程空闲时,状态分为以下两种情况:

  • 设置了核心线程的存活时间 :核心线程在空闲时会处于WAITING状态,等待任务。如果等待的时间超过了线程存活时间,则会将该线程从线程池的工作线程集合中移除,线程状态变为TERMINATED
  • 没有设置核心线程的存活时间 :一直处于WAITING状态。 当队列中有任务时,这些线程会被唤醒,状态由WAITING变为RUNNABLE。 查看获取任务源码
java 复制代码
// ThreadPoolExecutor
private Runnable getTask() {
		// timeouted标识是否超过线程存活时间
    boolean timedOut = false;
    for (;;) {
        // ...

        // 如果「设置了核心线程的存活时间」或者是「线程数量超过了核心线程数量」,则 timed 为 true。
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        // 扣减线程数量。
        // wc > maximuimPoolSize:线程池中的线程数量超过最大线程数量。其中 wc(worker count) 为线程池中的线程数量。
        // timed && timeOut:timeOut 表示获取任务超时。
        // 分为4种情况,这4种情况都会扣减线程
        // 1、线程数量超过最大线程数 && 线程数大于1
        // 2、设置了存活时间 && 已经超出存活时间 && 线程数大于1
        // 3、设置了存活时间 && 已经超出存活时间 && 任务队列中没有任务
        // 4、线程数量超过最大线程数 && 任务队列中没有任务
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            // 如果 timed 为 true,则使用 poll() 获取任务;否则,使用 take() 获取任务。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            // 获取任务之后返回。
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

线程池的拒绝策略有哪些?

  • AbortPolicy:抛出RejectedExecutionException
  • CallerRunsPolicy:提交任务的线程自己执行任务,也就是直接在调用execute方法的线程中执行任务。如果该线程已经终止,则会丢弃该任务。这种策略会降低新任务的提交速度,影响程序的整体性能。如果你的应用场景可以容忍此延迟并且你要求任何一个任务都要被执行的话,就可以选择这个策略。
  • DiscardPolicy:直接丢弃新任务
  • DiscardOldestPolicy:丢弃最早的未处理的任务,再将新任务放入任务队列。

如果不允许丢弃任务,应该选择哪个拒绝策略?

CallerRunsPolicy 源码如下:

java 复制代码
public static class CallerRunsPolicy implements RejectedExecutionHandler {

        public CallerRunsPolicy() { }


        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            //只要当前程序没有关闭,就用执行execute方法的线程执行该任务
            if (!e.isShutdown()) {

                r.run();
            }
        }
    }

CallerRunsPolicy拒绝策略有什么风险?如何解决?

如果触发了CallerRunsPolicy的任务是一个非常耗时的任务,导致后续任务长时间无法提交,大量的任务相关的对象堆积在内存,严重的情况下可能导致OOM。 解决方案如下: 1、简单粗暴的解决方案,增加任务队列的容量,调大堆内存,调大最大线程数。 2、但是如果服务器资源已经达到可利用的极限时,上面的方案就不再起作用了。所以就有了第二种方案,任务持久化,包括但不限于:

  • 设计一张任务表,并将触发了拒绝策略的任务存储到数据库中
  • 将触发了拒绝策略的任务放到缓存中
  • 将触发了拒绝策略的任务发到消息队列

以下介绍第一种方案的实现逻辑: 1、实现RejectedExecutionHandler接口自定义拒绝策略,自定义拒绝策略,该策略将在任务队列已满时,将新来的任务入库。 2、继承BlockingQueue实现一个混合式阻塞队列,该队列的成员变量中包含一个JDK自带的ArrayBlockingQueue。重写take()方法,也就是重写取任务的逻辑,取任务时优先从数据库中读取最早的任务,数据库中无任务再从上面提到的ArrayBlockingQueue中取任务。

这个问题有一些框架也给出了自己的解决方案,比如Netty,它的拒绝策略时直接创建一个线程池之外的线程处理任务,不过这种方案需要非常好的硬件资源,并且需要忍受临时创建的线程无法做到准确监控的缺点。源码如下:

java 复制代码
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
    NewThreadRunsPolicy() {
        super();
    }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        try {
            //创建一个临时线程处理任务
            final Thread t = new Thread(r, "Temporary task executor");
            t.start();
        } catch (Throwable e) {
            throw new RejectedExecutionException(
                    "Failed to start a new thread", e);
        }
    }
}

ActiveMQ则是尝试在限定时间内将任务入队,超时之后则抛异常。源码如下:

java 复制代码
new RejectedExecutionHandler() {
                @Override
                public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) {
                    try {
                        executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
                    } catch (InterruptedException e) {
                        throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
                    }
                    throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
                }
            });

线程池常用的阻塞队列有哪些?

  • 容量为Integer.MAX_VALUELinkedBlockingQueue(无界阻塞队列)
  • 没有容量,不存储元素的SynchronousQueue
  • DelayedWorkQueue:延迟队列,内部采用堆数据结构,按照任务的延迟时间长短对任务进行排序,是一个无界队列,底层虽然是数组,但是当数组容量不足时,它会自动进行扩容。
  • ArrayBlockingQueue:有界阻塞队列,底层由数组实现,容量一旦创建,就不能修改。

如何设计一个能够根据任务优先级来执行的线程池?

使用PriorityBlockingQueue(优先级阻塞队列)作为任务队列。 PriorityBlockingQueue是一个支持优先级的无界阻塞队列,可以看作是线程安全的PriorityQueue,两者底层使用的都是小顶堆形式的二叉堆的数据结构,即值最小的元素在堆顶(值越小的元素越优先出队)。 要想让PriorityBlockingQueue实现对任务的排序,方式有两种: 1、提交到线程池的任务实现Comparable接口,并且重写compareTo方法来指定任务的优先级比较规则。代码示例:

java 复制代码
import java.util.concurrent.*;

public class PriorityTask implements Runnable, Comparable<PriorityTask> {
    private final int priority;
    private final String name;

    public PriorityTask(int priority, String name) {
        this.priority = priority;
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("Executing task: " + name + " with priority: " + priority);
    }

    @Override
    public int compareTo(PriorityTask other) {
        return Integer.compare(this.priority, other.priority); // 优先级值越小,优先级越高
    }
}

// 这个包装不是必须,用原本的构造函数即可
public class PriorityThreadPoolExecutor extends ThreadPoolExecutor {
    public PriorityThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, new PriorityBlockingQueue<Runnable>());
    }
}

2、创建PriorityBlockingQueue时传入一个Comparator来指定任务的优先级比较规则。代码示例:

java 复制代码
import java.util.concurrent.*;

public class Task implements Runnable {
    private final int priority;
    private final String name;

    public Task(int priority, String name) {
        this.priority = priority;
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println("Executing task: " + name + " with priority: " + priority);
    }

    public int getPriority() {
        return priority;
    }
}

public class PriorityThreadPoolExecutor extends ThreadPoolExecutor {
    public PriorityThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit,
              new PriorityBlockingQueue<>(11, Comparator.comparingInt(Task::getPriority)));
    }
}

不过,这存在一些风险,比如:

  • PriorityBlockingQueue是无界的,可能堆积大量的任务,导致OOM
  • 可能会导致饥饿问题,也就是说,优先级低的任务可能长时间无法执行
  • PriorityBlockingQueue需要对队列中的元素进行排序,并且需要保证线程安全(出队入队都是需要上锁的,采用的是ReentrantLock,因此会降低性能。 OOM这个问题还是有解决办法的,只需要写一个自定义的队列类继承PriorityBlockingQueue并且重写offer方法,当插入的元素超过指定值就返回false即可。 饥饿问题可以优化一下设计,比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升(比较麻烦)。 性能问题的话无法避免,毕竟需要排序操作。
相关推荐
石去皿2 小时前
算法面试通关指南:高频考点+解题模板+避坑实战
算法·面试·职场和发展
努力学算法的蒟蒻3 小时前
day85(2.14)——leetcode面试经典150
面试·职场和发展
NEXT0612 小时前
React 闭包陷阱深度解析:从词法作用域到快照渲染
前端·react.js·面试
知识即是力量ol16 小时前
口语八股——MySQL 核心原理系列(终篇):SQL优化篇、日志与主从复制篇、高级特性篇、面试回答技巧总结
sql·mysql·面试·核心原理
UrbanJazzerati17 小时前
Python 导包、分包完全教程
后端·面试
苏婳66619 小时前
销售类结构化面试题库
面试·职场和发展·求职·找工作·面试题目
不想秃头的程序员19 小时前
父传子全解析:从基础到实战,新手也能零踩坑
前端·vue.js·面试
知其然亦知其所以然1 天前
别再死记硬背!一篇讲透 Zookeeper 的 Watcher 机制
后端·zookeeper·面试
闻哥1 天前
Elasticsearch查询优化实战:从原理到落地的全方位调优指南
java·大数据·elasticsearch·搜索引擎·面试·全文检索·springboot