ThreadPoolExecutor线程池管理器

ThreadPoolExecutor是ExecutorService的一个实例,是一个线程池管理器,能够通过调用线程池里面的线程来执行异步任务。

ThreadPoolExecutor主要用来解决两个痛点。一,当执行大量异步任务时,线程池能够减少线程创建和切换的开销,提高性能。二,当有过多的异步任务执行时,程序性能可能因为资源瓶颈降低,甚至直接挂掉。而ThreadPoolExecutor提供了一种管理线程资源的方法,它能够控制线程创建的数量,甚至是拒绝新任务。另外,ThreadPoolExecutor也提供一些数据统计功能,例如已完成任务数量、当前线程数目等。

为了适应各种应用场景,ThreadPoolExecutor提供了一系列可配置参数,例如corePoolSize(核心线程数目)、maximumPoolSize(最大线程数目)、keepAliveTime(线程空闲时间)、workQueue(任务存放队列)等。

下面让我们来理解一下这几个参数的作用。

首先是corePoolSize和maximumPoolSize。一个ThreadPoolExecutor会根据corePoolSize和maximumPoolSize设置的边界条件,自动的调整线程池里面线程的数量。调整规则如下:当一个新任务通过execute方法提交了,如果此时线程池里的线程数量少于corePoolSize,那么线程池会直接创建一个新线程来执行该任务,即使线程池里面存在空闲线程。继续提交任务,如果某时刻线程池里面的线程数量大于corePoolSize但小于maximumPoolSize,那么当且仅当工作队列(workQueue)满了的时候才会创建新的线程。如果线程数量达到maximumPoolSize且全部线程都是运行状态,继续提交任务,则会根据RejectedExecutionHandler来处理。

为了理解线程池线程数量的变化规则,我们看一下下面的这个演示程序。

package com.code.threadPool;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test {

	public static void main(String[] args) {
		int corePoolSize = 2;
		int maximumPoolSize = 5;
		int keepAliveTime = 10;
		BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(4, true);

		ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
				TimeUnit.SECONDS, workQueue);

		test(poolExecutor, 2);
		
		//执行完所有任务后关闭线程池
		poolExecutor.shutdown();
	}

	static void test(ThreadPoolExecutor poolExecutor, int taskCount) {
		for (int i = 0; i < taskCount; i++) {
			poolExecutor.submit(new TestRunnable());

			System.out.println("总任务数目:" + poolExecutor.getTaskCount() + ",已完成任务数目:"
					+ poolExecutor.getCompletedTaskCount() + ",线程池线程数目:" + poolExecutor.getPoolSize()
					+ ",线程池处于运行状态的线程数目:" + poolExecutor.getActiveCount());
		}
	}

}

class TestRunnable implements Runnable {
	@Override
	public void run() {
		/*try {
			TimeUnit.SECONDS.sleep(1l);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}*/
		
		System.out.println(Thread.currentThread().getName()+"执行完毕");
	}
}

我们初始化了一个corePoolSize=2,maximumPoolSize=5,workQueue大小为4的线程池,线程空闲时间为10秒。然后我们调整test方法的参数taskCount,看一下程序运行结果。

首先,测试taskCount=2的结果如下。对比上面的线程创建规则,我们可以看到,当我们的任务很快执行,空闲线程依然存活。此时,我们提交第二个任务,虽然线程池里面存在空闲线程,但由于线程数目<corePoolSize(=2),此时仍然会创建新的线程。

总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
pool-1-thread-1执行完毕
总任务数目:2,已完成任务数目:1,线程池线程数目:2,线程池处于运行状态的线程数目:1
pool-1-thread-2执行完毕

然后我们打开TestRunnable里面注释的那段代码,增加任务的执行时间,测试taskCount=5,结果如下。我们发现,虽然任务的总数目大于线程池线程数目,但由于workQueue未满(理论值为3,原因可以自己想想),线程池并不会创建新的线程。

总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
总任务数目:2,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:3,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:4,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:5,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
pool-1-thread-2执行完毕
pool-1-thread-1执行完毕
pool-1-thread-1执行完毕
pool-1-thread-2执行完毕
pool-1-thread-1执行完毕

接下来测试taskCount=7,结果如下。分析一下,我们的corePoolSize=2,可以执行两个任务,workQueue的大小为4,可以放入4个任务,这样一共是6个任务。那么,当第7个任务到来的时候,workQueue就没地方放了,因此只能创建新的线程来执行,线程池线程数目就变成3了。其实,workQueue满说明任务很多,线程池线程不够,因此需要创建新的线程了。

总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
总任务数目:2,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:3,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:4,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:5,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:6,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:7,已完成任务数目:0,线程池线程数目:3,线程池处于运行状态的线程数目:3
pool-1-thread-1执行完毕
pool-1-thread-3执行完毕
pool-1-thread-2执行完毕
pool-1-thread-3执行完毕
pool-1-thread-1执行完毕
pool-1-thread-2执行完毕
pool-1-thread-3执行完毕

最后,我们测试taskCount=10的情况,结果如下。我们的maximumPoolSize为5,最多处理5个任务,workQueue的大小为4,最多可以放入4个任务,这样一共是9个任务。想一下,第10个任务来了怎么处理?此时,既没有空闲线程,workQueue也满了,线程数达到最大,也不能创建新线程。这个时候,线程池会根据RejectedExecutionHandler指定的策略来处理,默认策略为AbortPolicy,它会抛出RejectedExecutionException异常。

总任务数目:1,已完成任务数目:0,线程池线程数目:1,线程池处于运行状态的线程数目:1
总任务数目:2,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:3,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:4,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:5,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:6,已完成任务数目:0,线程池线程数目:2,线程池处于运行状态的线程数目:2
总任务数目:7,已完成任务数目:0,线程池线程数目:3,线程池处于运行状态的线程数目:3
总任务数目:8,已完成任务数目:0,线程池线程数目:4,线程池处于运行状态的线程数目:4
总任务数目:9,已完成任务数目:0,线程池线程数目:5,线程池处于运行状态的线程数目:5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3d4eac69 rejected from java.util.concurrent.ThreadPoolExecutor@42a57993[Running, pool size = 5, active threads = 5, queued tasks = 4, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
	at com.code.threadPool.Test.test(Test.java:27)
	at com.code.threadPool.Test.main(Test.java:19)
pool-1-thread-4执行完毕
pool-1-thread-1执行完毕
pool-1-thread-5执行完毕
pool-1-thread-2执行完毕
pool-1-thread-3执行完毕
pool-1-thread-1执行完毕
pool-1-thread-2执行完毕
pool-1-thread-4执行完毕
pool-1-thread-5执行完毕

如果线程池已经关闭,或者线程池已经饱和(线程数目达到maximumPoolSize,workQueue已满),此时如果继续添加任务,此时会触发RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)。RejectedExecutionHandler可以理解成线程池拒绝服务的一个回调策略,它有下面四种策略:1.AbortPolicy,线程池的默认策略,直接抛出RejectedExecutionException异常。2.CallerRunsPolicy,调用execute的线程自己去执行提交的任务。该策略其实是一个反馈控制,降低了向线程池提交任务的速度。3.DiscardPolicy,直接丢弃任务。4.DiscardOldestPolicy,丢弃处于workQueue队列头位置的任务,然后尝试重新执行execute方法。

Java里面任何一个对象都有创建方法,线程池里面的线程也不例外。ThreadPoolExecutor里面的线程池是通过一个线程工厂ThreadFactory创建的,如果不制定,则会默认使用Executors.defaultThreadFactory,其实现逻辑见下面代码。通过该线程工厂创建的线程,都处于相同的ThreadGroup,优先级都为Thread.NORM_PRIORITY,且都是非守护线程。如果需要自己定义线程的名字、优先级、线程组、守护状态,我们可以创建自己的线程工厂。

static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

下面我们来看一下keepAliveTime这个参数,该属性跟TimeUnit一起,指定了超过corePoolSize的线程的最大存活时间。如果超过该时间,线程就会被销毁。注意,默认情况下,该参数只对超过corePoolSize的线程生效。

最后,我们来看一下workQueue这个参数,类型为BlockingQueue。workQueue主要是用来存放提交的任务,主要分为下面三种。一、SynchronousQueue。该模式下,被提交的任务会直接交给线程池处理,队列不存放任何任务。为了防止线程池拒绝提交的任务,通常需要设置一个无界的maximumPoolSizes,以此保证提交的任务能够获得线程去执行。二、无界队列。当所有的corePoolSize的线程都忙,新提交的任务会直接进入队列等待。因此,该模式下线程的数量不会超过corePoolSize,此时的maximumPoolSizes实际上并无意义。三、有界队列。通过设置maximumPoolSizes,可以设置最大线程数目,防止资源耗尽。该模式下,maximumPoolSizes和workQueue的大小会相互影响:使用较大容量的队列,较小的maximumPoolSizes,可以降低CPU消耗率、系统资源使用率、线程切换开销,但有可能导致低吞吐量。另一方面,使用较小的队列,通常需要较大的线程数目maximumPoolSizes,这会导致CPU持续繁忙。

ThreadPoolExecutor构造器参数的多样性也使得它的使用较为复杂,为了简化线程池的使用,jdk给我们提供了一个更为简单的线程池的工厂类Executors。

猜你喜欢

转载自blog.csdn.net/u014730001/article/details/81365943