ThreadPoolExecutor参数解释及流程

1.构造方法


public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) 


2.参数解释

  • corePoolSize

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

  • maximumPoolSize

线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;

  • keepAliveTime

线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;

  • unit

keepAliveTime的单位

  • workQueue

用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:

1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;

2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene

3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;

4、priorityBlockingQuene:具有优先级的无界阻塞队列;

  • threadFactory

它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

可以通过线程工厂给每个创建出来的线程设置更有意义的名字。线程池的命名时通过给这个factory增加组前缀来实现的。在虚拟机栈分析时,就可以知道线程任务由哪个线程工厂产生的。

使用 Google Guava设置线程名字

new ThreadFactoryBuilder().setNameFormat(&quot;XX-task-%d&quot;).build();

自定义实现:ThreadFactory


public class NamedThreadFactory implements ThreadFactory {
    
    

    /**
     *原子操作保证每个线程都有唯一的
     */
    private static final AtomicInteger threadNumber=new AtomicInteger(1);

    private final AtomicInteger mThreadNum = new AtomicInteger(1);

    private final String prefix;

    private final boolean daemoThread;

    private final ThreadGroup threadGroup;

   public NamedThreadFactory() {
    
    
        this("rpcserver-threadpool-" + threadNumber.getAndIncrement(), false);
    }

    public NamedThreadFactory(String prefix) {
    
    
        this(prefix, false);
    }


    public NamedThreadFactory(String prefix, boolean daemo) {
    
    
        this.prefix = StringUtils.isNotEmpty(prefix) ? prefix + "-thread-" : "";
        daemoThread = daemo;
        SecurityManager s = System.getSecurityManager();
        threadGroup = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup();
    }

    @Override
    public Thread newThread(Runnable runnable) {
    
    
        String name = prefix + mThreadNum.getAndIncrement();
        Thread ret = new Thread(threadGroup, runnable, name, 0);
        ret.setDaemon(daemoThread);
        return ret;
    }
}

  • handler

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;

当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

友好的拒绝策略:

  • 保存到数据库进行削峰填谷。在 空闲时再提取出来执行
  • 转向某个提示页
  • 打印日志

3.执行流程

在这里插入图片描述
在这里插入图片描述
1、如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(执行这一步骤要获取全局锁)
2、如果运行的线程等于或多于,则将 任务加入BlockingQueue
3、如果无法将任务将入BlockingQueue(队列已满),则创建新的线程来处理任务(需获取全局锁)
4、如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

4.关闭线程池

(1) 可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

(2) 只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

5.线程池的监控

(1) 可以监控以下属性

  • taskCount:线程池需要执行的任务数量
  • completedTaskCount: 线程池在运行过程中已完成的任务数量
  • largestPoolSize: 线程池里曾经创建过的最大线程数量
  • getPoolSize: 线程池的线程数量
  • getActiveCount:获取活动的线程数

(2) 监控的代码示例

public class MonitorThreadPoolUtil implements Runnable
{
    
    
    private ThreadPoolExecutor executor;
    private int seconds;
    private boolean run=true;

    public MonitorThreadPoolUtil(ThreadPoolExecutor executor, int delay)
    {
    
    
        this.executor = executor;
        this.seconds=delay;
    }
    public void shutdown(){
    
    
        this.run=false;
    }
    @Override
    public void run()
    {
    
    
        while(run){
    
    
            System.out.println(
                    String.format(&quot;[monitor] 池大小:%d,核心数:%d, 活跃数: %d, 完成数: %d, 任务数: %d, 线程结束没: %s, 任务结束没: %s&quot;,
                            this.executor.getPoolSize(),
                            this.executor.getCorePoolSize(),
                            this.executor.getActiveCount(),
                            this.executor.getCompletedTaskCount(),
                            this.executor.getTaskCount(),
                            this.executor.isShutdown(),
                            this.executor.isTerminated()));
            try {
    
    
                Thread.sleep(seconds*1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }

    }
}

6. 使用线程池的注意事项

1、合理设置各类参数,应根据实际业务场景来设置合理的工作线程数

在这里插入图片描述
线程资源必须通过线程池提供,不允许在应用中自行显示创建线程

创建线程或线程池时请指定有意义的线程名字,方便出错时回溯

建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。

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

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

假设,我们现在有一个Web系统,里面使用了线程池来处理业务,在某些情况下,系统里后台任务线程池的队列和线程池全满了,不断抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞,任务积压在线程池里。

如果当时我们设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。

史上最全的并发编程脑图:https://www.processon.com/view/5d43e6cee4b0e47199351b7f

猜你喜欢

转载自blog.csdn.net/fd2025/article/details/108449515
今日推荐