一、ThreadPoolExecutor:

线程池实现类ThreadPoolExecutor是Executor框架最核心的类,继承自Executor接口,里面有一个execute方法,用来执行线程,线程池主要提供一个队列,队列中保存着所有等待状态的线程。

ThreadPoolExecutor类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法基础上产生的。

public ThreadPoolExecutor(int corePoolSize,//核心线程数
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//空闲存活时间
                          TimeUnit unit,//时间单位
                          BlockingQueue<Runnable> workQueue,//线程池任务队列
                          ThreadFactory threadFactory,//创建线程的工厂
                          RejectedExecutionHandler handler) {//拒绝策略
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), handler);
}

参数介绍:

1、corePoolSize 核心线程数: 是指线程池中长期存活的线程数(如果当前核心线程数没有达到corePoolSize,即使当前核心线程池有空闲线程,也会创建新的线程来执行提交任务。如果调用了prestartCoreThread()或者prestartAllCoreThread(),线程池创建的时候所有的核心线程都会被创建并启动);

2、maximumPoolSize 最大线程数: 线程池允许创建的最大线程数量,当线程数池的任务队列满了之后,可以创建的最大线程数;

注意:最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常。

3、keepAliveTime,空闲线程存活时间: 如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗;

4、TimeUtil,时间单位: 空闲线程存活时间的描述单位,此参数配合参数3使用;

TimeUtil有7个参数:

  • TimeUnit.DAYS:天
  • TimeUnit.HOURS:小时
  • TimeUnit.MINUTES:分
  • TimeUnit.SECONDS:秒
  • TimeUnit.MILLISECONDS:毫秒
  • TimeUnit.MICROSECONDS:微妙
  • TimeUnit.NANOSECONDS:纳秒

5、BlockingQueue 阻塞队列: 线程池存放任务的队列,用来存储线程池的所有待执行任务。

它可以设置以下几个值:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列,按照先入先出的原则对元素进行排序,不保证线程公平访问队列,使用较少。
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列,队列默认和最长长度为Integer.MAX_VALUE,队列按照先入先出的原则对元素进行排序,使用较多。
  • 吞吐量通常要高于ArrayBlockingQueue
  • Executors.newFixedThreadPool()使用了这个队列
  • SynchronousQueue:一个不存储元素(无容量)的阻塞队列,即直接提交给线程不保持它们,每一个put操作都必须等待一个take操作,否则不能继续添加元素。支持公平访问队列,常用于生产者消费者模型,吞吐量较高,使用较多。
  • 每个插入操作必须等到另一个线程的移除操作,否则插入操作一直处于阻塞状态
  • 吞吐量通常要高于LinkedBlockingQueue
  • Executors.newCachedThreadPool使用了这个队列
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,使用较少。
  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

比较常用的是LinkedBlockingQueue,线程池的排队策略和BlockingQueue息息相关。

6、ThreadFactory 线程工厂: 线程池创建线程调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。

线程工厂的使用实例如下:

public static void main(String[] args) {
    //创建线程工厂    
    ThreadFactory threadFactory = new ThreadFactory() {        
        @Override
        public Thread newThread(Runnable r) {
            //创建线程池中的线程            
            Thread thread = new Thread(r);            
            //设置线程名称            
            thread.setName("Thread-" + r.hashCode());            
            //设置线程优先级(最大值:10)
            thread.setPriority(Thread.MAX_PRIORITY);            
            //......            
            return thread;        
        }    
    };    
    //创建线程池    
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10, 
            0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory);
    //使用自定义的线程工厂
    threadPoolExecutor.submit(new Runnable() {        
        @Override        
        public void run() {
            Thread thread = Thread.currentThread();
            System.out.println(String.format("线程:%s,线程优先级:%d", thread.getName(), thread.getPriority()));
        }    
    });
}

以上程序运行结果:

线程:Thread-495623552,线程优先级:10
Process finished with exit code -1

从上述执行结果可以看出,自定义线程工厂起作用了,线程的名称和线程优先级都是通过线程工厂设置的。

7、RejectedExecutionHandler 拒绝策略: 当线程池任务超出线程池队列可以存储的最大值后,执行的策略。

默认有以下四种策略:

  • AbortPolicy:拒绝并抛出异常;
  • CallerRunsPolicy:使用当前调用的线程来执行此任务;
  • DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务;
  • DiscardPolicy:忽略并抛弃当前任务。

线程池的默认策略是:AbortPolicy(拒绝并抛出异常)。

二、工作过程:

JAVA 核心线程数最大线程数线程池队列关系 java线程池最大线程数量_java


1、通过execute方法添加任务时,如果工作线程数小于corePoolSize,则创建一个新线程并执行该任务。注:新创建的线程会通过while循环不断从阻塞队列获取任务执行。

2、如果工作线程数大于等于corePoolSize,则将任务添加到阻塞队列。

3、如果阻塞队列满了,则判断工作线程是否小于maximumPoolSize,如果小于则创建新线程并执行该任务,否则调用handler拒绝策略。

三、执行任务逻辑:(原码分析)

线程调用runWork方法,通过while循环从getTask方法获取任务并执行。只要getTask方法不返回null,线程就不会退出。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    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);
    }
}

四、获取任务过程:

getTask方法实际上是从workQueue阻塞队列取任务。如果工作线程数大于corePoolSize,则会调用workQueue的poll方法获取任务,超时时间是keepAliveTime,如果超过了keepAliveTime时长,poll返回了null,上面提到的while循环就会退出,线程也就退出了。如果工作线程数小于等于corePoolSize,则会调用workQueue的take方法阻塞在当前直到有新的任务进来。

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

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

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

五、使用方法:

1、直接new一个对象:

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                8, 8, 0, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(30), 
                Executors.defaultThreadFactory(), 
                new ThreadPoolExecutor.CallerRunsPolicy());

2、通过Spring配置:

<bean id="prestoTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="4"/>
        <property name="maxPoolSize" value="8"/>
        <property name="keepAliveSeconds" value="200"/>
        <property name="queueCapacity" value="100"/> <!-- 拒绝策略 -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
        </property>
        <property name="threadNamePrefix" value="prestoThread"/>
    </bean>

六、合理的配置线程池:

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  • 任务的性质:CPU密集型任务,IO密集型任务和混合型任务;
  • 任务的优先级:高、中和低;
  • 任务的执行时间:长、中和短;
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理:

  • CPU密集型任务配置尽可能少的线程数量,如配置CPU个数+1的线程数的线程池。
  • IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如配置两倍CPU个数+1。
  • 混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。

当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。

最佳线程数目 = (线程等待时间+线程CPU时间) / 线程CPU时间 * CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

线程池大小最好是corePoolSize和maximumPoolSize保持一致,不要超过CPU核心数的3倍,如果纯计算任务(包括同机房的redis服务),只有很少的网络或者磁盘IO,那么线程池的大小设置为CPU的2倍比较合理。