常用线程池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>());
}
  1. corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池,是其优势;
  2. keepAliveTime = 0 该参数默认对核心线程无效,而FixedThreadPool全部为核心线程;
  3. workQueue 为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。
  4. 如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢
  • 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()等还有很多方法,如果大家想深入了解可以看源码,你会发现这能处理很多资金有限的情况,钱不够就靠优化代码了,哈哈。

四、使用线程池时怎么确定最佳线程数,以及高并发下会出现的问题

  1. 低并发时如何确认最佳线程数最优呐?
最佳线程数目 = ((线程等待时间+线程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()); //拒绝策略