常用线程池ThreadPoolExecutor类 和 线程池工厂类Executors。在1.5JDK 版本就提供了Executor,用来提供线程池。 可以使用 工厂类 Executors 工具类来创建线程池。一般通过ThreadPoolExecutor 来完成线程池的使用。 在 阿里巴巴的编码规范和其他的文章中,都推荐使用 工具类 Executors 来对 ThreadPooExecutor 进行实例化,而不建议开发者直接对 ThreadPoolExecutor 进行实例化。
一、使用场景很多,最近我在做附件上传接口,由于和其他公司合作,接口提供单个附件上传,如果正常一个上传附件很慢。如果用线程直接调用,由于功能硬件资源有限,导致服务不支持太多线程同时调用,所以用了线程池,且子线程调用完后我有操作,所以写了一个测试实例。
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
//ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10));
for (int k = 0, ksjze = 999; k < ksjze; k++) {
final int currentThreadNum = k;
Runnable run = new Runnable() {
@Override
public void run() {
try {
//执行方法
//do
logger.info("子线程[" + currentThreadNum + "]开始执行");
} catch (Exception e) {
e.printStackTrace();
} finally {
logger.info("子线程[" + currentThreadNum + "]结束");
}
}
};
executor.execute(run);
}
logger.info("已经开启所有的子线程");
executor.shutdown();
logger.info("shutdown():启动一次顺序关闭,执行以前提交的任务,但不接受新任务。");
while (true) {
if (executor.isTerminated()) {
logger.info("所有的子线程都结束了!");
//你还可以继续做你的事哟
break;
}
}
}
二、实例化的方法有几个常用的,以下是源码,做了个简单对比:
- newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池,是其优势;
- keepAliveTime = 0 该参数默认对核心线程无效,而FixedThreadPool全部为核心线程;
- workQueue 为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。
- 如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢
- newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序。通过这个方法创建的线程池允许线程在死亡或者发生异常后,重新启动一个新的线程来代替原来的线程并继续执行下去。
- newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】
三、从上面实例化函数看出最终操作的都是ThreadPoolExecutor,以下是源码,构造函数也有多个,参数个数不同我就不一一列举了。我们来看看这些参数的直观意思:
构造方法参数说明:
1.corePoolSize:核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将
allowCoreThreadTimeOut设置为true。
2.maximumPoolSize:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque
时,这个值无效。
3.keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收。
4.unit:指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。
5.workQueue:线程池中的任务队列,常用的有三种队列。
a.SynchronousQueue:是一种无缓冲的等待队列,在某次添加元素后必须等待其他线程取走后才能继续添加;
b.LinkedBlockingDeque:是一个无界缓存的等待队列,不指定容量则为Integer最大值,锁是分离的;
c.ArrayBlockingQueue:是一个有界缓存的等待队列,必须指定大小,锁是没有分离的;
6.threadFactory:线程工厂,提供创建新线程的功能,通过线程工厂可以对线程的一些属性进行定制。
7.RejectedExecutionHandler:当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的
rejectedExecution方法,线程池有以下四种拒绝策略。
a.AbortPolicy:当任务添加到线程池中被拒绝时,它将抛出RejectedExecutionException 异常。
b.CallerRunsPolicy:当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。
c.DiscardOldestPolicy:当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加
到等待队列中。
d.DiscardPolicy:当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。
以下是构造方法之一:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
线程里比如上面用到的executor.execute(run)、executor.shutdown()等还有很多方法,如果大家想深入了解可以看源码,你会发现这能处理很多资金有限的情况,钱不够就靠优化代码了,哈哈。
四、使用线程池时怎么确定最佳线程数,以及高并发下会出现的问题
- 低并发时如何确认最佳线程数最优呐?
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
我们的服务器CPU核数为8核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:( 80 + 20 )/20 * 8 = 40。也就是设置 40个线程数最佳。
\2. 当然在高并发的场景下,如果大家使用这些静态方法创建线程池,会有一些问题哟。我上面说的这几类适用低并发场景,低并发很难出现OOM问题。
- newFixedThreadPool:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- newSingleThreadExecutor:允许请求的队列长度是Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- newCachedThreadPool:允许创建的线程数是Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
例如写出类似以下的代码:
public class Test {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}
}
}
使用newFixedThreadPool创建的线程池,是会有坑的,它默认是无界的阻塞队列,如果任务过多,会导致OOM问题。运行一下以上代码,出现了OOM。
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.example.dto.NewFixedTest.main(NewFixedTest.java:14)
这是因为newFixedThreadPool使用了无界的阻塞队列的LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo代码设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终出现OOM。
看下newFixedThreadPool的相关源码,是可以看到一个无界的阻塞队列的,如下:
//阻塞队列是LinkedBlockingQueue,并且是使用的是无参构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//无参构造函数,默认最大容量是Integer.MAX_VALUE,相当于无界的阻塞队列的了
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
那我们该怎办呢?工作中,建议大家自定义线程池,并使用指定长度的阻塞队列。
优先推荐使用ThreadPoolExecutor类,我们自定义线程池。
具体代码如下:
ExecutorService threadPool = new ThreadPoolExecutor(
8, //corePoolSize线程池中核心线程数
10, //maximumPoolSize 线程池中最大线程数
60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue(500), //队列
new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略