JAVA并发编程系列(11)线程池底层原理架构剖析

面试官:说说JAVA线程池的几个核心参数?

之前我们用了10篇文章详细剖析了synchronized、volatile、CAS、AQS、ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、并发锁、Condition等各个核心基础原理,今天开始我们说说并发领域的各种工具包还有应用场景。

1、为什么要用线程池?

日常频繁创建和销毁线程是非常消耗系统资源的操作,会降低系统的整体性能。而线程池通过池化的管理思想,把线程的创建、任务调度、任务执行、任务等待、任务拒绝、销毁等全生命周期各个环节都进行统一管理,大幅提升系统性能,提高线程利用率以及任务响应。

2、线程池核心参数意义

一共有6个:

arduino 复制代码
    private volatile ThreadFactory threadFactory;
  
    private volatile RejectedExecutionHandler handler;
    
    private volatile long keepAliveTime;//非核心线程空闲超时时间

    private volatile boolean allowCoreThreadTimeOut;

    private volatile int corePoolSize;//核心线程数量

    private volatile int maximumPoolSize;//最大线程数量
2.1 corePoolSize 核心线程数量

核心线程即使空闲,也不会被回收。如果当前线程数量小于corePoolSize,即使其他核心线程空闲,线程池也会新增创建一个线程来执行任务。

核心线程会一直存活在线程池里,但是如何设置了线程池的allowCoreThreadTimeOut为true,则核心线程空闲一定时间也会被回收。

arduino 复制代码
private volatile boolean allowCoreThreadTimeOut
2.2 maximumPoolSize最大线程数量

线程池最大线程数量maximumPoolSize = 核心线程数量corePoolSize + 非核心线程数量。

场景A:当线程数量>=核心线程数量,且等待队列未满,将任务加入到等待队列。

场景B:当队列已满,并且线程数量<maximumPoolSize ,就新增非核心线程。这种非核心线程就是在空闲时间大于keepAliveTime,就会被回收。

场景C:当队列已满,且线程数量大于等于maximumPoolSize,就触发拒绝策略handler拒绝任务。

2.3 keepAliveTime 线程空闲时间

keepAliveTime允许线程的最大的空闲存活时间。如果一个非核心线程在空闲状态下持续超过keepAliveTime了,就会被回收,以及线程池设置allowCoreThreadTimeOut为true,核心线程也会被回收。

2.4 workQueue任务存储队列

用于存储等待执行的任务。主要有三种类型。

第一个,直接提交SynchronousQueue,把任务直接提交给工作线程而不放到等待队列。这个队列不存储元素,newCachedThreadPool使用的就是这种同步移交队列,吞吐量比LinkedBlockingQueue大。

第二个,LinkedBlockingQueue无界队列,队列的最多容量为int的最大值,相当于无限容量。newFixedThreadPool和newSingleThreadPool使用的就是LinkedBlockingQueue,这个吞吐量比ArrayBlockingQueue高。

第三个,ArrayBlockingQueue有界队列,可以有效避免资源耗尽。

除了这三种,还有DelayQueue、PriorityBlockingQueue。

2.5 threadFactory线程工厂

线程工厂,用来创建线程。

2.6 handler拒绝策略

拒绝任务的策略。当线程数量达到最大线程数量maximumPoolSize,以及等待队列workQueue也满了,这时候需要用来拒绝任务提交时的策略handler。策略类型有抛异常、用调用者所在的线程执行任务、丢弃队列中第一个任务执行当前任务、直接丢弃任务)。

策略种类有:

AbortPolicy:直接抛出异常。

DiscardPolicy:丢弃任务,不抛异常。

DiscardOldestPolicy:将等待队列里等待最久的任务丢弃。

CallerRunsPolicy: 哪个线程提交的任务,哪个线程去处理。

4、有几种线程池?

JAVA线程池ThreadPoolExcutor,常见的有这四种线程池。

scss 复制代码
//初始化一个无限线程的线程池,无需等待队列
Executors.newCachedThreadPool()
//初始化一个支持周期性运行的线程池
Executors.newScheduledThreadPool(10);
//初始化固定数目线程的线程池
Executors.newFixedThreadPool(10);
//初始化仅有一个线程的线程池
Executors.newSingleThreadExecutor();
4.1 Executors.newCachedThreadPool()可缓存线程池

创建一个可缓存的线程池,如果线程数量大于处理任务时,空闲线程被回收;当提交任务增加时,又可以新建线程去处理任务。这种线程池的线程数无限制,corePoolSize数值为0, maximumPoolSize 的数值都是为 Integer.MAX_VALUE。

线程可复用性很高,可以减少频繁创建/销毁线程,减少系统开销。工作队列workQueue选用SynchronousQueue。

4.2 Executors.newScheduledThreadPool()定时调度线程池

也是固定长度的线程池,但是支持以延迟或者定时的方式去执行任务。

4.3 newSingleThreadExecutor()单线程的线程池

一个线程,corePoolSize 和 maximumPoolSize 的数值都是为 1,线程池里只有一个工作线程执行任务。若这个唯一的线程异常出问题了,会新建另一个线程来替代。所有任务在等待队列是FIFO顺序被执行。

4.4 Executors.newFixedThreadPool()固定长度线程池

每次提交任务的时候就会创建一个新的线程,直到达到线程池的最大数量限制,如何任务大于线程数量,就进入等待队列。 corePoolSize 和 maximumPoolSize 的数值相等。工作队列选用LinkedBlockingQueue。

最后,我们上一个线程池demo,固定线程池,每次最多N个线程在执行任务,其他任务等待。

java 复制代码
package lading.java.mutithread;

import cn.hutool.core.date.DateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 固定线程池,线程数量为3
 * 提交多个count+1任务计算
 *
 */
public class Demo013ThreadPool {
    //计算任务count
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        //固定数量线程池
        ExecutorService service = Executors.newFixedThreadPool(3);
        Executors.newScheduledThreadPool(10);
        //提交20次,对count+1的任务
        for (int i = 1; i < 20 + 1; i++) {
            service.submit(new Thread(() -> {
                //模拟每次计算耗时2s
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //完成本次对count+1计算任务
                System.out.println(DateTime.now().toString("YYYY-MM-dd hh:mm:ss") + " " +Thread.currentThread().getName() + "线程 执行计算任务:" + count.incrementAndGet());
            }));
        }
        service.shutdown();
    }
}

每次只有三个线程在运行任务,其他任务等之前任务执行完成后再执行。

今天就分享到这,明天分享并发容器CurrentHashMap。

相关推荐
eason_fan3 分钟前
前端手撕代码(bigo)
算法·面试
WeiLai11126 分钟前
面试基础---Redis 延迟队列深度解析
java·redis·分布式·后端·junit·面试·架构
天涯过客TYGK7 分钟前
unity console日志双击响应事件扩展
java·unity·junit
Root06249 分钟前
【笔记】记一次easyExcel中注解ExcelProperty映射字段赋值无效问题
java·开发语言·笔记
一个努力学习的小男孩10 分钟前
java环境部署
java·开发语言
光影少年11 分钟前
react中NavLink和a标签区别
react.js·面试
轻浮j12 分钟前
ReentrantLock原理(源码解析)
java
黑暗也有阳光14 分钟前
springboot利用Redisson 实现缓存与数据库双写不一致问题
spring boot·redis·面试
lqstyle15 分钟前
面试必备之redis过期策略和内存淘汰策略
后端·面试
硅谷神农15 分钟前
分库分表:数据爆炸时的计划生育政策
面试