Executors是个工具类,里边有实现好的可以直接拿来用的几个线程池。
这几个种类的线程池实现本质上都是用的new ThreadPoolExecutor的不同参数组合的几个重载方法实例化出来的。
而ThreadPoolExecutor的类的关系如下,从抽象定义 -> 具体实现:
Executor -> ExecutorService -> AbstractExecutorService -> ThreadPoolExecutor
所以先得弄清楚ThreadPoolExecutor的构造函数的参数的意义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
- corePoolSize 核心线程、即常驻线程的个数
- maximumPoolSize 线程池中最多允许多少个线程
- keepAliveTime 超过核心线程数的线程能够空闲存在多长时间,超过了之后会被关闭。
- workQueue 本线程池用的阻塞队列
好了,接下来具体看看Executors提供的线程池实现就容易多了。
Executors.newSingleThreadExecutor()
“只有一个线程,并发的任务数超过1个就放一个巨长的队列里排队”
实际上对应的是new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())
所以newSingleThreadExecutor是一个只有1个线程的线程池,内部用的是LinkedBlockingQueue这样一个准无界队列。长度准确说是int64的最大值,Integer.MAX_VALUE : 2147483647
Executors.newFixedThreadPool(N)
“有N个线程,并发任务超过N个就放入一个巨长的队列里排队”
new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue())
所以newFixedThreadPool是一个有N个核心线程的线程池,最多也只能有N个线程,同样用LinkedBlockingQueue准无界队列。
Executors.newCachedThreadPool()
“来一个任务就会启一个线程去处理,几乎可以同时启动无限多的线程并发处理任务”
new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
所以newCachedThreadPool是不设置核心线程个数的,而最多可以允许Integer.MAX_VALUE这么多线程,每个线程允许空闲60秒。
这种线程池用的阻塞队列比较特殊,是无容量的SynchronousQueue,再加上coreSize=0,
也就是相当于来一个任务就往队列里放、马上就会发现队列满了放不进去,然后就要创建非核心线程,有1个非核心线程创建才能有1个任务放到队列里。(队列无容量,相当于就是起个传递的作用)
所以,如果每个任务执行时间比较长,千万不能使用这种线程池,不然短时间如果任务提交的比较多的话会创建出大量的线程出来!
还是通过一个例子来理解:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
int taskNum = 100; //任务数量
logger.info("向CachedThreadPool提交"+taskNum + "个任务");
CountDownLatch latch = new CountDownLatch(taskNum);
for(int i=0; i<taskNum; i++) { //向线程池提交100个任务
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
logger.info("线程"+Thread.currentThread().getName()+"已运行");
try {
TimeUnit.SECONDS.sleep(1800); //模拟任务阻塞
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}});
}
latch.await();//让主线程阻塞等待所有任务执行完,便于观察运行情况
cachedThreadPool.shutdown();
logger.info(taskNum+"个任务都执行完了");
上面的例子是向CachedThreadPool连续提交了100个任务,每个任务会sleep阻塞比较长的时间,这样对于这个池,同时并发的任务就有100个,结果真就启动了100个线程在跑。日志里可以看到“线程pool-1-thread-100已运行”。
接下来,我们去掉每个任务的sleep阻塞,让每个任务以很快的速度执行完成。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
int taskNum = 100; //任务数量
logger.info("向CachedThreadPool提交"+taskNum + "个任务");
CountDownLatch latch = new CountDownLatch(taskNum);
for(int i=0; i<taskNum; i++) { //向线程池提交100个任务
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
logger.info("线程"+Thread.currentThread().getName()+"已运行");
latch.countDown();
}});
}
latch.await();//让主线程阻塞等待所有任务执行完,便于观察运行情况
cachedThreadPool.shutdown();
logger.info(taskNum+"个任务都执行完了");
运行结果:
向CachedThreadPool提交100个任务
线程pool-1-thread-1已运行
线程pool-1-thread-3已运行
线程pool-1-thread-2已运行
线程pool-1-thread-4已运行
线程pool-1-thread-1已运行
线程pool-1-thread-4已运行
线程pool-1-thread-2已运行
。。。
线程pool-1-thread-27已运行
100个任务都执行完了
也执行了100个任务,不过线程号最大只有pool-1-thread-27,存在一个线程执行了多次任务的情况,即线程复用。这不难理解,由于每个任务执行时间比较短,使得后面任务提交给线程池的时候前面的任务已经执行完了、对应的线程空闲,一牛网在线直接拿来复用执行当前提交的任务了。这符合我们对CachedThreadPool的认识。
Executors.newScheduledThreadPool(N)
“有N个核心线程,可以有几乎无限个非核心线程,进入队列待处理的任务具有延时消费特性”
最终构造方法是用的ThreadPoolExecutor(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
指定了核心线程数量,最大允许创建Integer.MAX_VALUE个线程,非核心线程执行完毕之后立刻关闭。
用的任务队列是DelayedWorkQueue,这个队列中元素在添加到队列之前必须先通过实现Delayed接口指定多久才能从队列中获取元素。
也就是到规定的时间才能从队列里消费元素。
看一下jdk里边官方给的例子(有少许改动):
CountDownLatch latch = new CountDownLatch(1);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1);
Runnable beeper = new Runnable() {
@Override
public void run() {
logger.info("beep");
}
};
//首次执行于10秒后开始,之后每隔10秒执行一次beeper任务
ScheduledFuture scheduledFuture = scheduledThreadPool.scheduleAtFixedRate(beeper, 10, 10, TimeUnit.SECONDS);
//2分钟后取消上述每隔10秒的定时任务
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
scheduledFuture.cancel(true);
logger.info("取消10秒一次的定时任务");
latch.countDown();
}}, 2*60, TimeUnit.SECONDS);
latch.await();
logger.info("定时任务已取消,关闭scheduledThreadPool线程池");
scheduledThreadPool.shutdown();
logger.info("scheduledThreadPool线程池已关闭");
运行结果:
beep
beep
beep
beep
beep
beep
beep
beep
beep
beep
取消10秒一次的定时任务
定时任务已取消,关闭scheduledThreadPool线程池
scheduledThreadPool线程池已关闭
btw,如果是使用execute或者submit方法,那么对于scheduledThreadPool来说就是任务没有延迟立即执行。这时候就跟普通的线程池一样了。
Executors.newWorkStealingPool(ParallelLevel)
Creates a thread pool that maintains enough threads to supportthe given parallelism level,
and may use multiple queues toreduce contention.
The parallelism level corresponds to the maximum number of threads actively engaged in,
or available toengage in, task processing. The actual number of threads may grow and shrink dynamically.
A work-stealing pool makes noguarantees about the order in which submitted tasks are executed.
jdk 1.8提供了一个新的WorkStealingPool,工作窃取池。
创建一个线程池,用于维护足够的线程以支持给定的并行度级别,并且可以使用多个队列来减少争用。并行度级别对应于实际或可提供的参与任务处理的最大线程数量。实际线程的数量可能动态的扩缩容。偷工池并不保证任务的执行顺序是按照它们被提交的顺序来的。
工作窃取线程池,其实是个帮忙分担工作的概念,我们知道jdk的线程池在阻塞队列没填满之前,都是核心线程在干活的,这样很容易出现几个核心线程拼命干活,队列里又堆积了非常多的任务等待执行,把核心线程累死的情况。
想象一下只有1个核心线程,最大线程允许10个,阻塞队列是个无界队列。永远都是这1个核心线程干到死(所以说jdk线程池的这个算法有点问题,tomcat里边worker线程池就对算法做了优化)。但是明明允许的最大线程数还没有达到。所以工作窃取线程池的思路就是在允许的并行度范围之内,如果某一个线程等待执行的任务过多,那么其他线程会从它这里“窃取”任务来执行,一直达到最大并行度。
来看一个例子:
int parallelLevel = Runtime.getRuntime().availableProcessors();
logger.info("并行度为" + parallelLevel);
ExecutorService workStealingPool = Executors.newWorkStealingPool(parallelLevel);
for(int i=0; i<5; i++) {
final int taskIndex = i;
workStealingPool.execute(new Runnable() {
@Override
public void run() {
logger.info("线程"+Thread.currentThread().getName()+"正在执行任务"+taskIndex);
try {
TimeUnit.SECONDS.sleep(3600);
} catch (InterruptedException e) {e.printStackTrace();}
}});
}
运行结果:
并行度为4
线程ForkJoinPool-1-worker-1正在执行任务0
线程ForkJoinPool-1-worker-3正在执行任务2
线程ForkJoinPool-1-worker-2正在执行任务1
线程ForkJoinPool-1-worker-0正在执行任务3
笔者电脑CPU只有4个核心,所以这里并行度是4,每个任务是设置的长时间sleep阻塞的,4个线程都在被任务阻塞占用,所以最终任务4在阻塞队列里边等待执行。
Executors.newWorkStealingPool内部是通过
new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true)
返回的一个ForkJoinPool线程池,值得一提的是ForkJoinPool类本身提供了一个静态的全局ForkJoinPool.common供使用。ForkJoinPool.common的参数可以通过jvm参数java.util.concurrent.ForkJoinPool.common.xxxx进行配置设置。主要有:
参数 | 描述 | 默认值 |
---|---|---|
parallelism | 并行级别 | cpu核心数 |
threadFactory | 线程工厂类名 | ForkJoinPool.DefaultForkJoinWorkerThreadFactory |
exceptionHandler | 错误处理程序 | null |
maximumSpares | 最大允许额外线程数 | 256 |