详解线程池

线程池有着以下优势:

  1. 降低资源消耗:线程池中维护着多个创建好的线程,通过重复利用这些线程来降低线程创建与销毁带来的损耗
  2. 提高响应速度:当有任务需要执行时,可以立刻去执行这些任务,而不需要花时间去等待线程的创建【线程的创建是要获取全局锁的,并不是立刻就能创建出来】
  3. 实现对线程进行统一的管理

下面是使用线程池的一段常规代码:

java 复制代码
public static void main(String[] args) {
    // 创建线程池
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            16,
            32,
            1000,
            TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.DiscardPolicy());

    // 执行任务
    threadPoolExecutor.submit(()-> System.out.println("执行任务"));

    // 关闭线程池
    threadPoolExecutor.shutdown();
}

从上面这段代码切入,先了解下线程池的七大参数:

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数=核心线程数+临时线程数
  3. keepAliveTime:空闲线程存活时间。当线程数大于核心线程数时,空闲线程在等待新任务到达的最大时间,如果超过这个时间还没有任务请求,空闲线程就会被销毁
  4. unit:线程的存活时间的单位
  5. workQueue:阻塞队列,保存任务的队列
    • ArrayBlockingQueue:基于定长数组结构的有界队列
    • LinkedBlockingQueue:基于链表结构的有界队列
  6. threadFactory:线程工厂,用于生成线程池中工作线程的线程工厂,一般用默认的即可
  7. rejectHandler:拒绝策略,当阻塞队列和最大线程数都使用完了,就会拒绝之后提交的任务
    • CallerRunsPolicy:只要线程池没有关闭的话,则使用调用者所在的线程直接运行当前任务,通常表现为将任务交给主线程执行
    • AbortPolicy:丢弃任务并抛出拒绝执行 RejectedExecutionException 异常,也是线程池默认的拒绝策略
    • DiscardPolicy:直接丢弃任务
    • DiscardOldestPolicy:只要线程池没有关闭的话,丢弃阻塞队列中最老的一个任务,并将新任务加入

下面介绍下线程池的具体工作流程:

有两点说明:

  1. 如果阻塞队列选取的是无界队列,那就不会创建临时线程,任务会被一直保存到阻塞队列中
  2. 线程池中最开始是没有线程的,即使我们在创建线程池的时候指定好了线程数,但是依然不会立刻就创建好,而是当有任务提交到线程池中时,线程池在调用execute或者submit方法时才会创建出线程并执行任务

目前共有两种向线程池中提交任务的方法:execute和submit:

  • execute方法只能提交Runnable型任务,并且没有返回值
java 复制代码
public void execute(Runnable command) {}
  • submit方法既能提交Runnable型任务也能提交Callable型任务,线程池会返回一个future类型的对象
java 复制代码
public Future<?> submit(Runnable task) {}
java 复制代码
public <T> Future<T> submit(Callable<T> task) {

共有两种方法用于关闭线程池,shutdown和shutdownNow:

  • shutdown方法只会终止线程池中空闲的线程,不会终止那些正在执行任务的线程,并且已经提交到线程池中的任务也会被处理完成
  • shutdownNow方法会终止所有线程,不论线程是空闲的还是正在执行任务

线程池的工作状态:

  • RUNNING:运行状态,能够接收新任务以及处理已添加到任务队列中的任务。线程池一旦被创建,其初始状态就为RUNNING
  • SHUTDOWN:不再接收新任务,但能处理已添加到任务队列中的任务
  • STOP:不再接收新任务,也不处理已添加到任务队列中的任务,并且会中断正在处理的任务
  • TIDYING:所有的任务都已终止,所有的工作线程也已经被销毁
  • TERMINATED:线程池彻底停止工作

Worker:

java 复制代码
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
        
    Worker(Runnable firstTask) {
        setState(-1);
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
        runWorker(this);
    }

}
  • 线程池中的工作线程以Worker作为体现,真正执行任务的线程thread是Worker的成员变量,线程thread在执行任务时,最终会去执行Worker中的run方法,Worker中的run方法又调用了runWorker方法

上面这些内容都是泛泛而谈,也都是表层的一些论述,下面聚焦到一些细节点进行分析:

问题一:当向线程池中提交一个任务时,线程池是如何处理这个任务的?

  • 线程池提供了execute和submit两种方式进行任务的提交,submit底层也是基于execute实现的,从源码切入进行解释:
java 复制代码
public void execute(Runnable command) {
	if (command == null)
		throw new NullPointerException();
	// 获取当前ctl值
	int c = ctl.get();
	// 当前线程数少于最大核心线程数
	if (workerCountOf(c) < corePoolSize) {
		// 创建核心线程并添加线程任务
		if (addWorker(command, true))
			return;
		//获取最新的ctl值
		c = ctl.get();
	}
	// 如果线程池状态为RUNNING并且成功向工作队列添加任务
	if (isRunning(c) && workQueue.offer(command)) {
		// 再次获取最新的ctl值
		int recheck = ctl.get();
		// 如果此时线程池状态不处于RUNNING状态 则需要移除之前往队列中添加的任务
		if (! isRunning(recheck) && remove(command))
			// 执行任务拒绝策略
			reject(command);
		// 如果此时没有可用的工作线程 则创建新的工作线程
		else if (workerCountOf(recheck) == 0)
			addWorker(null, false);
	}
	// 添加非核心线程来执行线程任务
	// 当线程数超过最大或者线程池状态不是RUNNING时
	else if (!addWorker(command, false))
		// 执行任务拒绝策略
		reject(command);
}
java 复制代码
// 线程池内部实际上不区分核心线程和非核心线程 其实都是线程
// 通过变量core这种"核心"的概念 对线程池中线程的创建频率进行调控
private boolean addWorker(Runnable firstTask, boolean core) {
	retry:
	// 无限循环
	for (;;) {
		int c = ctl.get();
		// 获取线程池状态
		int rs = runStateOf(c);
            //1.当线程池的状态处于STOP、TIDYING、TERMINATED时,线程池是拒绝执行任何任务的,因此不需要任务,也不添加线程
            //2.当线程池的状态处于SHUTDOWN状态时,线程池需要把已添加的任务处理完因此,如果任务队列还有任务的话,需要继续添加线程来加快处理,但是不能在接受新的任务
		if (rs >= SHUTDOWN &&
			! (rs == SHUTDOWN &&
			   firstTask == null &&
			   ! workQueue.isEmpty()))
			return false;

		for (;;) {
			// 获取工作线程数量
			int wc = workerCountOf(c);
                    // 1.线程数达到上限(CAPACITY)
                    // 2.如果此时创建的是核心线程,则需要与corePoolSize进行比较;
                    // 3.如果此时创建的是非核心线程,则需要与maximumPoolSize进行比较
			if (wc >= CAPACITY ||
				wc >= (core ? corePoolSize : maximumPoolSize))
				return false;
			// 通过CAS操作增加线程数(workerCount),并且跳出循环
			if (compareAndIncrementWorkerCount(c))
				break retry;
             // 如果上面的CAS操作失败 则获取最新的ctl值 并检查线程池状态与开始是否一致
             // 如果一致,继续执行此for循环,否则重新执行retry代码块,
             // 自旋直到CAS操作成功,后续才能添加线程
			c = ctl.get(); 
			if (runStateOf(c) != rs)
				continue retry;
		}
	}

	//剩余关键代码
	//将任务添加到创建的Worker中 并启动线程
	w = new Worker(firstTask);
	final Thread t = w.thread;
	t.start();
}

问题二:线程池是如何实现线程复用的?

  • addWorker方法最终会创建出一个Worker,并启动对应的线程thread,当线程分配到CPU时间片后将会开启任务的执行过程。前面说到线程thread在执行任务时,执行的是Worker中的run方法,Worker中的run方法又调用了runWorker方法
java 复制代码
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); 
    boolean completedAbruptly = true;
    try {
        // 线程首先是执行自己的task 
        // 如果task为null 则去任务队列中getTask,这里的while循环是线程复用的关键!
        while (task != null || (task = getTask()) != null) {
            w.lock()
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 执行task
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                // 任务完成数加一
                // 释放锁
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}
java 复制代码
private Runnable getTask() {
    //超时标志
    boolean timedOut = false;

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

        // 如果线程池状态为SHUTDOWN并且任务队列为空
        // 如果线程池状态为STOP、TIDYING、TERMINATED 不论任务队列还有没有任务
        // 此时移除当前worker并且线程数量减1
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        // 获取当前工作线程数
        int wc = workerCountOf(c);

        // allowCoreThreadTimeOut默认为false 可设置
        // 当allowCoreThreadTimeOut为true或者当前线程数大于核心线程数 timed为true
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        try {
            Runnable r = timed ?
            // 非阻塞方法,超出一定时间未取到任务时,直接返回null
            workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
            // 阻塞方法,不会返回,没有任务会一直等待,使得线程未被闲置,而是时刻处于一个可复用的状态!
            workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
  • 当allowCoreThreadTimeOut为false,wc <= corePoolSize时,此时执行workQueue.take()获取任务
  • 当allowCoreThreadTimeOut为false,wc > corePoolSize时,此时执行workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)获取任务
  • 当allowCoreThreadTimeOut为true,此时执行workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)获取任务

所以所谓的【线程复用】是在allowCoreThreadTimeOut为false,且线程池中的线程数小于等于核心线程数,此时才是线程复用,因为外层有个while循环,并且线程会一直阻塞着等待任务的到来;假设当前线程池中的线程数大于核心线程数,线程在一定的时间内未取到任务就结束了,while循环也会结束,也就不存在线程的复用了。

问题三:程执行任务过程中出现异常是怎么处理的?会影响其他线程吗?

java 复制代码
public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    // 把Callable型任务转换成了FutureTask型任务
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}
  • submit方法把传入的Callable类型任务转换成了FutureTask类型任务,紧接着调用了execute方法,在execute方法中执行任务的run方法,而此时调用的是FutureTask中的run方法
java 复制代码
public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                // 出现异常时并没有抛出异常,而是调用了setException方法
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 将异常赋给outcome
        // outcome用于存储任务的执行结果
        outcome = t;
        // state的值代表了任务在运行过程中的状态
        // 任务执行过程中:state为COMPLETING
        // 任务正常执行完成:state为NORMAL
        // 任务执行过程中有异常:state为EXCEPTIONAL
    	// 任务被删除:state为CANCELLED
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); 
        finishCompletion();
    }
}
java 复制代码
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

private V report(int s) throws ExecutionException {
    // 获取outcome的值
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    // 抛出异常
    throw new ExecutionException((Throwable)x);
}
  • execute方法,当任务执行过程中出现异常时,直接抛出异常
  • submit方法,当任务执行过程中出现异常时,必须调用Future.get()方法才可以抛出异常

对于execute和submit两种方式,当线程池中的某个线程出现异常时,并不会影响到其余线程的正常工作。只需要看runWorker方法遇到异常时的处理逻辑:

java 复制代码
} finally {
        processWorkerExit(w, completedAbruptly);
    }
java 复制代码
private void processWorkerExit(Worker w, boolean completedAbruptly) {

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // completedTaskCount记录线程池总共完成的任务
        // w.completedTasks表示的是当前线程完成的任务数
        completedTaskCount += w.completedTasks;
        // 移除当前线程对应的worker
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    // 获取最新的ctl值
    int c = ctl.get();
    // 检查线程池状态,如果此时线程池处于RUNNING或者SHUTDOWN
    if (runStateLessThan(c, STOP)) {
    	// min表示线程池允许的最小线程数量 取决于allowCoreThreadTimeOut的值
        int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
        // 如果最小线程数量是0 但是此时任务队列还有任务
        // 那么最小线程数量至少为1 去处理任务队列中的任务
        if (min == 0 && ! workQueue.isEmpty())
            min = 1;
        // 如果此时的工作线程数量大于等于最小值 直接返回
        if (workerCountOf(c) >= min)
            return; 
        // 因为线程被中断或者当前工作线程数量小于最小线程数
        // 此时都需要创建一个新的线程
        addWorker(null, false);
    }
}

可见:如果线程出现异常,则会将该线程从线程池中移除销毁,有需要会再创建一个新线程加入到线程池中,也就是说在任务发生异常的时候,会终结掉运行它的线程

问题四:线程池中为什么要使用到阻塞队列而不是直接去创建线程?

  • 因为无限制的创建线程会出现内存不足的问题,并且线程越多,线程的上下文切换就会很频繁;其次线程池在创建线程的时候,是需要获取全局锁的,会影响并发效率,这个时候如果要创建其他线程会被阻塞,进而影响整体的工作效率。
  • 阻塞队列自带阻塞与唤醒功能,当阻塞队列中没有任务需要执行时,此时阻塞队列会阻塞住当前线程【线程处于waiting状态】,此时线程会释放占有的cpu资源;并且当阻塞队列中有任务时会唤醒阻塞住的线程,线程去获取并执行其中的任务

问题五:为什么使用阻塞队列而不是非阻塞队列

  • 因为阻塞队列可以保证当任务队列中没有任务时会阻塞住获取任务的线程,使线程进入wait状态,释放cpu资源;当队列中有任务时才唤醒对应线程从队列中获取任务并执行,使得在线程不至于一直占用着cpu资源
  • 如果是非阻塞线程,线程并不会进入wait状态,也就不会释放cpu资源,而是一直不断的循环去阻塞队列中获取任务,这就造成了CPU的资源浪费
相关推荐
微信-since8119211 分钟前
[ruby on rails] 安装docker
后端·docker·ruby on rails
代码吐槽菌2 小时前
基于SSM的毕业论文管理系统【附源码】
java·开发语言·数据库·后端·ssm
豌豆花下猫2 小时前
Python 潮流周刊#78:async/await 是糟糕的设计(摘要)
后端·python·ai
YMWM_2 小时前
第一章 Go语言简介
开发语言·后端·golang
码蜂窝编程官方3 小时前
【含开题报告+文档+PPT+源码】基于SpringBoot+Vue的虎鲸旅游攻略网的设计与实现
java·vue.js·spring boot·后端·spring·旅游
hummhumm3 小时前
第 25 章 - Golang 项目结构
java·开发语言·前端·后端·python·elasticsearch·golang
J老熊3 小时前
JavaFX:简介、使用场景、常见问题及对比其他框架分析
java·开发语言·后端·面试·系统架构·软件工程
AuroraI'ncoding3 小时前
时间请求参数、响应
java·后端·spring
好奇的菜鸟3 小时前
Go语言中的引用类型:指针与传递机制
开发语言·后端·golang
Alive~o.03 小时前
Go语言进阶&依赖管理
开发语言·后端·golang