池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
《Java 并发编程的艺术》提到的使用线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
Executor 框架
Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。
this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。
Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。
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();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ThreadPoolExecutor 3 个最重要的参数:
- corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
- maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor其他常见参数 :
- keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
- unit : keepAliveTime 参数的时间单位。
- threadFactory :executor 创建新线程的时候会用到。
- handler :饱和策略。当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时触发饱和策略。
ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy :不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。
ThreadPoolExecutor源码分析
public class ThreadPoolExecutor extends AbstractExecutorService {
// 控制变量-存放状态和线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 任务队列,必须是阻塞队列
private final BlockingQueue<Runnable> workQueue;
// 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合
private final HashSet<Worker> workers = new HashSet<>();
// 全局锁
private final ReentrantLock mainLock = new ReentrantLock();
// awaitTermination方法使用的等待条件变量
private final Condition termination = mainLock.newCondition();
// 记录峰值线程数
private int largestPoolSize;
// 记录已经成功执行完毕的任务数
private long completedTaskCount;
// 线程工厂,用于创建新的线程实例
private volatile ThreadFactory threadFactory;
// 拒绝执行处理器,对应不同的拒绝策略
private volatile RejectedExecutionHandler handler;
// 空闲线程等待任务的时间周期,单位是纳秒
private volatile long keepAliveTime;
// 是否允许核心线程超时,如果为true则keepAliveTime对核心线程也生效
private volatile boolean allowCoreThreadTimeOut;
// 核心线程数
private volatile int corePoolSize;
// 线程池容量
private volatile int maximumPoolSize;
// 省略其他代码
}
ThreadPoolExecutor状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// 通过ctl值获取运行状态
private static int runStateOf(int c) { return c & ~COUNT_MASK; }
// 通过ctl值获取工作线程数
private static int workerCountOf(int c) { return c & COUNT_MASK; }
// 通过运行状态和工作线程数计算ctl的值,或运算
private static int ctlOf(int rs, int wc) { return rs | wc; }
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
// CAS操作线程数增加1
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
// CAS操作线程数减少1
private boolean compareAndDecrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect - 1);
}
// 线程数直接减少1
private void decrementWorkerCount() {
ctl.addAndGet(-1);
}
接下来分析一下线程池的状态变量,工作线程上限数量位的长度是COUNT_BITS,它的值是Integer.SIZE - 3,也就是正整数29:
我们知道,整型包装类型Integer实例的大小是4 byte,一共32 bit,也就是一共有32个位用于存放0或者1。
在ThreadPoolExecutor实现中,使用32位的整型包装类型存放工作线程数和线程池状态。
其中,低29位用于存放工作线程数,而高3位用于存放线程池状态,所以线程池的状态最多只能有2^3种。
工作线程上限数量为2^29 - 1,超过5亿,这个数量在短时间内不用考虑会超限。
接着看工作线程上限数量掩码COUNT_MASK,它的值是(1 < COUNT_BITS) - l,也就是1左移29位,再减去1,如果补全32位,它的位视图如下:然后就是线程池的状态常量,这里只详细分析其中一个,其他类同,这里看RUNNING状态:
// -1的补码为:111-11111111111111111111111111111
// 左移29位后:111-00000000000000000000000000000
// 10进制值为:-536870912
// 高3位111的值就是表示线程池正在处于运行状态
private static final int RUNNING = -1 << COUNT_BITS;
ThreadPoolExecutor里面使用到JUC同步器框架,主要用于四个方面:
- 全局锁mainLock成员属性,是可重入锁ReentrantLock类型,主要是用于访问工作线程Worker集合和进行数据统计记录时候的加锁操作。
- 条件变量termination,Condition类型,主要用于线程进行等待终结awaitTermination()方法时的带期限阻塞。
- 任务队列workQueue,BlockingQueue类型,任务队列,用于存放待执行的任务。
- 工作线程,内部类Worker类型,是线程池中真正的工作线程对象。
几种常见的线程池详解
FixedThreadPool
// 创建一个线程池,该线程池重用固定数量的线程在共享的无界队列中运行。
// 在任何时候,最多nThreads线程将是活动的处理任务。
// 如果在所有线程都处于活动状态时提交了其他任务,它们将在队列中等待,直到有线程可用。
// 如果任何线程在关闭前的执行过程中因失败而终止,则在需要执行后续任务时,将有一个新线程取而代之。
// 池中的线程将一直存在,直到它被明确shutdown 。
// 参数:
// 线程核心数和最大数量都是 nThreads
// 存活时间为0,
// 队列:LinkedBlockingQueue(无界队列)可能堆积大量的请求,从而导致 OOM。
// 任务拒绝策略:AbortPolicy (拒绝抛异常)
// 线程工厂:java.util.concurrent.Executors.defaultThreadFactory
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
不推荐使用FixedThreadPool,FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
- 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
- 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPool 的 corePoolSize 和 maximumPoolSize 被设置为同一个值。
- 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
- 运行中的 FixedThreadPool(未执行 shutdown()或 shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。
SingleThreadExecutor
//创建一个 Executor,它使用单个工作线程在无界队列中运行。
// (但是请注意,如果这个单线程在关闭之前由于执行失败而终止,如果需要执行后续任务,一个新线程将取代它。)
// 保证任务按顺序执行,并且不会超过一个任务处于活动状态在任何给定的时间。
// 与其他等效的newFixedThreadPool(1) ,返回的执行器保证不可重新配置以使用其他线程。
// 参数:
// 线程核心数和最大数量都是 1
// 存活时间为0,
// 队列:LinkedBlockingQueue(无界队列)可能堆积大量的请求,从而导致 OOM。
// 任务拒绝策略:AbortPolicy (拒绝抛异常)
// 线程工厂:java.util.concurrent.Executors.defaultThreadFactory
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
从上面源代码可以看出新创建的 SingleThreadExecutor 的 corePoolSize 和 maximumPoolSize 都被设置为 1.其他参数和 FixedThreadPool 相同。
不推荐使用SingleThreadExecutor:
- SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。
- SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM,
CachedThreadPool
//创建一个线程池,根据需要创建新线程,但在可用时将重用先前构造的线程,并在需要时使用提供的 ThreadFactory 创建新线程。
// 参数:
// 线程核心数是0,最大核心数Integer.MAX_VALUE(可能会创建大量线程,从而导致 OOM)
// 存活时间为 60L, TimeUnit.SECONDS
// 队列:SynchronousQueue 具有非公平访问策略的SynchronousQueue
// 任务拒绝策略:AbortPolicy (拒绝抛异常)
// 线程工厂:java.util.concurrent.Executors.defaultThreadFactory
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
CachedThreadPool 的corePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。 这个在实际项目中基本不会被用到,也不推荐使用,大家只需要简单了解一下它的思想即可。
//创建一个线程池,可以安排命令在给定延迟后运行,或定期执行。
// 参数:
// 线程核心数是nThreads,最大线程数是Integer.MAX_VALUE(可能会创建大量线程,从而导致 OOM)
// 存活时间为0,
// 队列:DelayedWorkQueue(专门的延迟队列)
// 任务拒绝策略:AbortPolicy (拒绝抛异常)
// 线程工厂:java.util.concurrent.Executors.defaultThreadFactory
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
ScheduledThreadPoolExecutor 使用的任务队列 DelayQueue 封装了一个 PriorityQueue,PriorityQueue 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(ScheduledFutureTask 的 time 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(ScheduledFutureTask 的 squenceNumber 变量小的先执行)。
ScheduledThreadPoolExecutor运行机制
ScheduledThreadPoolExecutor 的执行主要分为两大部分:
- 当调用 ScheduledThreadPoolExecutor 的 scheduleAtFixedRate() 方法或者 scheduleWithFixedDelay() 方法时,会向 ScheduledThreadPoolExecutor 的 DelayQueue 添加一个实现了 RunnableScheduledFuture 接口的 ScheduledFutureTask 。
- 线程池中的线程从 DelayQueue 中获取 ScheduledFutureTask,然后执行任务。
ScheduledThreadPoolExecutor 为了实现周期性的执行任务,对 ThreadPoolExecutor做了如下修改:
- 使用 DelayQueue 作为任务队列;
- 获取任务的方不同
- 执行周期任务后,增加了额外的处理
ScheduledThreadPoolExecutor 和 Timer 的比较:
- Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor不是;
- Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 ScheduledThreadPoolExecutor 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程;
- 在TimerTask 中抛出的运行时异常会杀死一个线程,从而导致 Timer 死机:-( …即计划任务将不再运行。ScheduledThreadExecutor 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 afterExecute 方法ThreadPoolExecutor)。抛出异常的任务将被取消,但其他任务将继续运行。
队列
SynchronousQueue
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
拥有公平(FIFO)和非公平(LIFO)策略,非公平侧罗会导致一些数据永远无法被消费的情况?
使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作。
LinkedBlockingQueue
LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。
ArrayBlockingQueue
ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。
任务执行顺序
执行submit()或者execute()方法时,任务在线程池例如合适才执行呢?
Runnable vs Callable
Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。
工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result))。
@FunctionalInterface
public interface Runnable {
/**
* 被线程执行,没有返回值也无法抛出异常
*/
public abstract void run();
}
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果,或在无法这样做时抛出异常。
* @return 计算得出的结果
* @throws 如果无法计算结果,则抛出异常
*/
V call() throws Exception;
}
execute() vs submit()
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException。
shutdown()VSshutdownNow()
shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
isTerminated() VS isShutdown()
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
线程池线程数多少合理?
有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
线程
线程存在哪些状态,并且状态间如何转换?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态:
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用 start() 方法 |
RUNNABLE | 运行状态,Java 线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME WAITING | 超时等待状态,该状态不同于 WAITING,它是可以在指定的时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
由上图可以看出:
线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY (可运行) 状态。可运行状态的线程获得了 cpu 时间片 (timeslice) 后就处于 RUNNING(运行) 状态。
操作系统隐藏Java 虚拟机 JVM) 中的READY 和RUNNING 态,它只能看到RUNNABLE 状态,所以Java 系统一将这两个状态统称为RUNNABLE (运行中) 状态。
当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
而 TIME WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep (long millis) 方法或wait (long millis) 方法可以将Java 线程置于 TIMED WAITING 状态。当超时时间到达后Java 线程将会返回到 RUNNABLE 状态。
当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED (阳塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED (终止) 状态。
参考:
https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/concurrent/java-thread-pool-summary.md
https://www.throwx.cn/2020/08/23/java-concurrency-thread-pool-executor/