并发与多线程(四) --- 线程池

本文详细探讨了线程池的概念、好处及其在Java中的实现。线程池通过管理线程资源,避免频繁创建和销毁带来的性能损耗,提供任务调度和拒绝策略。文章深入分析了ThreadPoolExecutor的构造参数,如核心线程数、最大线程数、存活时间等,以及如何自定义线程工厂和拒绝策略。还介绍了Executors的几种线程池创建方式及其潜在风险,并给出了线程池源码的执行流程,帮助读者更好地理解和使用线程池。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


一、线程池的好处

线程使应用能够更加充分合理地协调利用CPU、内存、网络、I/O等系统资源。线程的创建需要开辟虚拟机栈、本机方法栈、程序计数器等线程私有的内存空间。在线程销毁时需要回收这些资源。 频繁地创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程自身无法解决的。 所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。

1.1 线程池的作用:

  • 利用线程池管理并复用线程、控制最大并发数等。
  • 实现任务线程队列缓存策略和拒绝机制
  • 实现某些与时间相关的功能,如定时执行、周期执行等
  • 隔离线程环境。比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大;因此,通过配置独立的线程池,将较慢的交易与搜索服务隔离开,避免各服务线程相互影响。

1.2 线程是如何创建的

首先从 ThreadPoolExecutor 构造方法分析,如何自定义ThreadFactory 和 RejectedExecutionHandler ,通过分析ThreadExecutor 的execute 和addWorker 两个核心方法,学习如何把任务线程加入到线程池中运行。
ThreadPoolExecutor 的构造方法如下:

public ThreadPoolExecutor(
	int corePoolSize,                            	// (第1个参数)
	int maximumPoolSize, 							// (第2个参数)
	long keepAliveTime, 							// (第3个参数)
	TimeUnit unit,									// (第4个参数)
	BlockingQueue<runnable> workQueue, 				// (第5个参数)
	ThreadFactory threadFactory, 					// (第6个参数)
	RejectedExecutionHandler handler) { 			// (第7个参数)

	if(corePoolSize < 0 ||
	// maximumPoolSize 必须大于或等于1 也要大于或等于 corePoolSize   (第1处)
	maximumPoolSize <= 0 ||
	maximumPoolSize < corePoolSize ||
	keepAliveTime < 0)
		throw new IllegalArgumentException();
	(2)
	if (workQueue == null || ThreadFactory == null || handler == null)
		throw new NullPointerException();
	// 其他代码 ...
}

  • 第1个参数: corePoolSize
    表示常驻核心线程池数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于 0, 即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。
  • 第2个参数:maximumPoolSize
    表示线程池能够容纳同时执行的最大线程数。从上方代码中第1处来看,必须大于或等于1。如果maximumPoolSize 与 corePoolSize 相等,即是固定大小线程池。
  • 第3个参数:keepAliveTime
    表示线程池中的线程空闲时间。当空闲时间达到keepAliveTime 值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程数大于 corePoolSize 时,keepAliveTime 才会起作用。但是当ThreadPoolExecutor 的allowCoreThreadTimeOut 变量设置为true 时,核心线程超时后也被回收
  • 第4个参数:TimeUnit
    表示时间单位。keepAliveTime 的时间单位通常是TimeUnit.SECONDS 。
  • 第5个参数:workQueue
    表示缓存队列。当请求的线程数大于corePoolSize 时,线程进入BlockingQueue 阻塞队列,BlockingQueue 队列缓存达到上限后,如果还有新任务需要处理,那么线程池会创建新的线程,最大线程数为 maximumPoolSize 。例如一个生产消费模型队列,使用LinkedBlockingQueue 是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取。
  • 第6个参数: threadFactory
    表示线程工厂。它用来生成一组相同任务的线程。线程池的命令是通过给这个factory 增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。
  • 第7个参数: handler
    表示执行拒绝策略的对象。当第5个参数workQueue 的任务缓存区到达上限后,并且活动线程数大于maximumPoolSize 的时候,线程池通过该策略处理请求,这是一种简单的限流保护。像某年双十一没有处理好访问流量过载时的拒绝策略,导致内部测试页面被展示出来,使用户手足无措。友好的拒绝策略可以是如下三种
    (1)保存到数据库进行削峰填谷。在空闲时在提取出来执行。
    (2)转向某个提示页面。
    (3)打印日志。

从第2处来看,队列、线程工厂、拒绝处理服务都必须有实例对象,但在实际编程中,很少有程序员对这三者进行实例化,而通过Executor 这个线程池静态工厂提供默认实现,那么Executor 与 ThreadPoolExecutor 是什么关系呢?线程池相关类图如下图所示:

在这里插入图片描述

/**
* @param 线程任务
* @throws RejectedExecutionException 如果无法创建任何状态的线程任务
*/
void execute (Runnable command);

ExecutorService 接口继承了Executor 接口,定义了管理线程任务的方法。ExecutorService 的抽象类 AbstractExecutorService 提供了submit()、invokeAll() 等部分方法的实现,但是核心方法Executor.execute() 并没有在这里实现。因为所有的任务都在这个方法里执行,不同实现会带来不同的执行策略,这一点在后续的ThreadPoolExecutor 解析时,会进一步分析。通过Executors 的静态工厂方法可以创建三个线程池的包装对象:ForkJoinPool、ThreadPoolExecutor、ScheduledThreadPoolExecutor。

1.3 Executors 分析

Executors 核心的方法由五个

  • Executors.newWorkStealingPool:
    JDK8 引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争,此构造方法中把CPU 数量设置为默认的并行度:
public statice ExecutorService newWorkStealingPool(){
	// 返回 ForkJoinPool (JDK7引入)对象,它也是AbstractExecutorService 的子类
	return new ForkJoinPool (Runtime.getRuntime().availableProcessors(),
	ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
  • Executors.newCachedThreadPool :
    maximumPoolSize 最大可以至Integer.MAX_VALUE , 是高度可伸缩的线程池,如果达到这个上限,相信没有服务器能够继续工作,肯定会抛出OOM 异常。keepAliveTime 默认为60 秒,工作线程处于空闲状态,则回收工作线程。如果认为数增加,再次创建出新线程处理任务。

  • Executors.newScheduledThreadPool:
    线程数最大至Integer.MAX_VALUE ,与上述相同,存在OOM 风险。它是ScheduledExecutorService 接口家族的实现类支持定时及周期性任务执行。相比Timer,ScheduledExecutorService 更安全,功能更强大,与newCachedThreadPool 的区别是不回收工作线程

  • Executors.newSingleThreadExecutor:
    创建一个单线程的线程池,相当于单线程串行执行所有任务,保证按任务的提交顺序依次执行。

  • Executors.newFixedThreadPool :
    输入的参数即是固定线程数,既是核心线程数也是最大线程数,不存在空闲线程,所以keepAliveTime 等于 0 :

public static ExecutorService newFixedThreadPool(int nThreads){
	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlokingQueue<runnable>());
}

这里,输入的队列没有指明长度,下面为LinkedBlockingQueue 的构造方法:

public LinkedBlockingQueue () {
	this(Integer.MAX_VALUE);
}

使用这样的无界队列,如果瞬间请求非常大,会有OOM 的风险。除newWorkStealingPool 外,其他四个创建方式都存在资源耗尽的风险

Executors 中默认的线程工厂和拒绝策略过于简单,通常对用户不够友好。线程工厂需要做创建前的准备工作,对线程池创建的线程必须明确标识,就像药品的生成批号一样,为线程本身指定有意义的名称和相应的序列号。拒绝策略应该考虑到业务场景,返回相应的提示或友好地跳转

1.3.1 ThreadFactory 示例

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);
        System.out.println(thread.getName());
        return thread;
    }

    public static void main(String[] args) {
        UserThreadFactory factory = new UserThreadFactory("abide");
        Task task = new Task();
        factory.newThread(task).start();
    }
}

// 任务执行体
class Task implements Runnable{

    private final AtomicLong count = new AtomicLong(0L);
    @Override
    public void run() {
        System.out.println("running_"+count.getAndIncrement());
    }
}

上述示例包括线程工厂和任务执行体的定义,通过newThread 方法快速、统一地创建线程任务,强调线程一定要有特定意义的名称,方便出错时回溯。

1.3.2 RejectedExecutionHandler 的简单实现

下面代码实现了RejectedExecutionHandler ,实现了接口的rejectedExecution 方法,打印出当前线程池状态,源码如下:

public class UserRejectHandler implements RejectedExecutionHandler {
	@Override
	public void rejectedExecution(Runnable task, ThreadPoolExecutor executor){
		System.out.println("task rejected. " + executor.toString());
	}
}

在ThreadPoolExecutor 中提供了四个公开的内部静态类:

  • AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException 异常。
  • DiscardPolicy:丢弃任务,但是不抛弃异常,这是不推荐的做法。
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中。
  • CallerRunsPolicy: 调用任务的run() 方法绕过线程池直接执行。

根据之前实现的线程工厂和拒绝策略,线程池的相关代码实现如下:

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);
        }

    }
}

执行结果如下:

UserThreadFactory’s 第1机房-Worker1
UserThreadFactory’s 第2机房-Worker1
UserThreadFactory’s 第1机房-Worker2
running_0
UserThreadFactory’s 第2机房-Worker2
running_1
running_2
running_3
running_4
running_6
running_5
running_7
running_8

task rejected. java.util.concurrent.ThreadPoolExecutor@3f99bd52[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 120]

当任务被拒绝的时候,拒绝策略会打印出当前线程池的大小已经达到了maximumPoolSize=2,且队列已满,完成的任务数提示已经有120个(最后一行)。

二、线程池源码详解

2.1 ThreadPoolExecutor 的源码分析

在ThreadPoolExecutor 的属性定义中频繁地用位移运算来表示线程池状态,位移运算时改变当前值的一种高效手段,包括左移与右移。下面从属性定义开始分析ThreadPoolExecutor 的源码:

// Integer 共有32位,最右边29位表示工作线程数,最左边3位表示线程池状态
// 注:简单地说,3个二进制可以表示从0至7的8个不同的数值     (第1处)
private static final int COUNT_BITS = Integer.SIZE - 3;

// 000-11111111111111111111111111111,类似于子网掩码,用于为的与运算,得到左边3位,还是右边29位
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

// 用左边3位,实现5种线程池状态。(在左3位之后加入中画线有助于理解)
// 111-00000000000000000000000000000,十进制值:-536,870,912
// 此状态表示线程池能接受新任务
private static final int RUNNING = -1 << COUNT_BITS;

// 000-00000000000000000000000000000,十进制:0
// 此状态不再接受新任务,但可以继续执行行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;

// 001-00000000000000000000000000000,十进制:536,870,912
// 此状态全面拒绝,并中断正在处理的任务
private static final int STOP= 1 << COUNT_BITS;

// 010-00000000000000000000000000000,十进制:1,073,741,824
// 此状态表示所有任务已经被终止
private static final int TIDYING = 2 << COUNT_BITS;

// 011-00000000000000000000000000000,十进制:1,610,612,736
// 此状态表示已清理完现场
private static final int TERMINATED= 3 << COUNT_BITS;

// 与运算,比如 001-00000000000000000000000100011,表示67个工作线程,
// 掩码取反:   111-00000000000000000000000000000,即得到左边3位001,表示线程池当前处于STOP 状态
private static int runStateOf(int c) { return c & ~COUNT_MASK; }

// 同理掩码 000-11111111111111111111111111111,得到右边29位,即工作线程数
private static int workerCountOf(int c) { return c &  COUNT_MASK; }

// 把左边3位与右边29位按或运算,合并成一个值
private static int ctlOf(int rs, int wc) {return rs | wc; }

第一处说明,线程池的状态用高3位表示,其中包括了符号位五种状态的十进制按从小到大依次排序为:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED ,这样设计的好处是可以通过比较值的大小来确定线程池的状态
例如程序中经常会出现isRunning 的判断:

private static boolean isRunning(int c) {
	return c < SHUTDOWN;
}

2.2 ThreadPoolExecutor 关于execute 方法的分析

我们知道Executor 接口有且只有一个方法execute ,通过参数传入待执行线程的对象。下面分析ThreadPoolExecutor 关于execute 方法的实现:

public void execute (Runnable comand){
        // 返回包含线程数及线程池状态的Integer 类型数值
        int c = ctl.get();
        // 如果工作线程数小于核心线程数,则创建线程任务并执行
        if (workerCountOf(c) < corePoolSize){
            // addWorker 是另一个极为重要的方法,  (第1处)
            if(addWorker(command, true))
                return;
            // 如果创建失败,防止外部已经在线程池中加入新任务,重新获取一下
            c = ctl.get();
        }
        
        // 只有线程池处于RUNNING 状态,才执行后半句: 置入队列
        if(isRunning(c) && WorkQueue.offer(command)){
            int recheck = ctl.get();
            // 如果线程池不是RUNNING 状态,则将刚加入队列的任务移出
            if(! isRunning(recheck) && remove(command))
                reject(command);
            // 如果之前的线程已被消费完,新建一个线程
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
            
            // 核心池和队列都已满,尝试创建一个新线程
        } else if (!addWorker(command, fasle))
            // 如果addWorker 返回时false,即创建失败,则唤醒拒绝策略   (第2处)
            reject(command);
    }

第1处: execute 方法在不同的阶段有三次addWorker 的尝试动作。
第2处: 发生拒绝的理由有两个:
(1) 线程池状态为非RUNNING 状态
(2) 等待队列已满

2.2.1 addWorker 方法源码分析

下面是ThreadPoolExecutor 类中addWorker 方法源码分析

/**
* 根据当前线程池状态,检查是否可以添加新的任务线程,如果key则创建并启动任务
* 如果一切正常则返回true。返回false 的可能性如下:
* 1.线程池没有处于RUNNING 状态
* 2.线程工厂创建新的任务线程失败
* 
* firstTask:外部启动线程池时需要构造的第一个线程,它是线程的母体
* core: 新增工作线程时的判断指标,解释如下:
* 	true  表示新增工作线程时,需要判断当前RUNNING 状态的线程是否少于 corePoolSize
* 	false  表示新增工作线程时,需要判断当前RUNNING 状态的线程是否少于 maximumPoolSize
*/
 private boolean addWorker(Runnable firstTask, boolean core) {
 		// 不需要任务预定义的语法标签,响应下文的continue retry,快速退出多层嵌套循环     (第1处)
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 参考之前的状态分析:如果是RUNNING 状态,则条件为假,不执行后面的判断
            // 如果状态不为SHUTDOWN ,或firstTask 初始线程不为空,或者队列为空,都会直接返回创建失败    (第2处)
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                // 如果超过最大允许线程数则不能再添加新的线程
                // 最大线程数不能超过2^29 ,否则影响左边3位的线程池状态值
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
				// 将当前活动线程数 +1    (第3处)
                if (compareAndIncrementWorkerCount(c))
                    break retry;

				// 线程池和工作线程数是可变化的,需要经常提取这个最新值
                c = ctl.get();  // Re-read ctl
                // 如果已经关闭,则再次从retry 标签处进入,在第2处再做判断  (第4处)
                if (runStateOf(c) != rs)
                    continue retry;
                // 如果线程还是处于RUNNING 状态,那就再说明仅仅是第3处失败 ,继续循环执行   (第5处)
            }
        }

		// 开始创建工作线程
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
        	// 利用Worker 构造方法中的线程池工厂创建线程,并封装成工作线程Worker 对象
            w = new Worker(firstTask);
            
            // 注意这是Worker 中的属性对象 thread   (第6处)
            final Thread t = w.thread;
            if (t != null) {
             	// 在进行ThreadPoolExecutor 的敏感操作时都需要持有住锁,避免在添加和启动线程时被干扰
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // 持有锁时重新检,在ThreadFactory失败或在获取锁之前关闭时退出。
                   
                    int rs = runStateOf(ctl.get());
					// 当线程池状态为RUNNING 或 SHUTDOWN 且firstTask 初始线程为空时
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // 预先检查t是否可启动
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        // 整个线程池在运行期间的最大并发任务个数
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
					// 终于看到亲切的start 方法了,注意:并非线程池的execute 的command 参数指向的线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
            	// 线程启动失败,把刚才第3 处加上的工作线程计数再减回去
                addWorkerFailed(w);
        }
        return workerStarted;
    }
  • 第1处:
    配合循环语句出现的label,类似于goto 作用。label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则会编译出错。目的是在实现多重循环时能够快速退出到任何一层。这种做法的出发点似乎非常贴心,但是在大型软件项目中,滥用标签行跳转的后果将是灾难性的。示例代码中,在retry 下方有两个无限循环,在workerCount 加1 成功后,直接退出两层循环。

  • 第3处:
    与第1处相呼应,AtomicInteger 对象的加1 操作时原子性的。break retry 表示直接跳出与retry 相邻的这个循环体。

  • 第4处:
    此continue 跳转至标签处,继续执行循环。如果条件为假,则说明线程池还处于运行状态,即继续在for(;;)循环内执行。

  • 第5处:
    compareAndIncrementWorkerCount 方法执行失败的概率非常低。即使失败,再次执行时成功的概率也是极高的,类似于自旋锁原理。这里的处理逻辑是先加1,创建失败再减1,这是轻量处理并发创建线程的方式。如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大。

  • 第6处:
    Worker 对象是工作线程的核心类实现,部分源码如下:

  private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
 	final Thread thread;
 	Runnable firstTask;
 	volatile long completedTasks;
 	Worker(Runnable firstTask) {
 	 	// 它是AbstractQueueSynchronizer 的方法,在runWorker 方法执行之前禁止线程被中断
        setState(-1); // 禁止中断,直到runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    
    // 当thread 被start() 之后,执行runWorker 的方法
    public void run() {
         runWorker(this);
    }

}

线程池的相关源码比较精炼,还包括线程池的销毁、任务提取和消费等,与线程状态图一样,线程池也有自己独立的状态转化流程。

总结

使用线程池要注意如下几点:

  1. 合理设置各类参数,应根据实际业务场景来设置合理的工作线程数。
  2. 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
  3. 创建线程或线程池时请指定有意义的线程名称,方面出错时回溯。

线程池不允许使用Executors,而是通过ThreadPoolExecutor 的方式创建,这样的处理方法能更加明确线程池的运行规则,规避资源耗尽的风险。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值