Java线程池的一点总结

966 阅读5分钟

主要用来做两件事

1、利用多核CPU的并发处理能力提高工作效率

2、避免线程的频繁创建和销毁带来的开销来提高工作效率

任务提交到线程池后的处理逻辑

1、提交任务到线程池

2、核心线程池是否已满,没满则创建核心线程数个线程来处理任务

3、如果核心线程数都在工作,新的任务进来先进入到等待队列

4、如果等待队列也满了,还有新的任务进来,看下当前线程数是否达到最大线程数,没有的话赶紧创建线程来执行队列中的任务

5、如果已达最大线程数且等待队列也放满了,还有任务进来,则按照定义好的拒绝策略来处理无法执行的任务

JDK默认提供的线程池

// 单个线程的线程池,多个任务会被缓存到队列中(队列长度Integer.MAX_VALUE)
public static ExecutorService newSingleThreadExecutor();
// 固定数量的线程池,单个线程的升级版,核心和最大线程数一样多,且等待队列是Integer.MAX_VALUE长度的链表
public static ExecutorService newFixedThreadPool(int nThreads);
// 带缓存的线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE,类似阻塞队列
public static ExecutorService newCachedThreadPool();
// 定时调度的线程池,返回值与上面3个线程池不同,为ScheduledThreadPoolExecutor,有3中执行方式
// scheduleAtFixedRate 每个调度任务会至少等待指定时间,任务执行的时间超过指定时间,则等待的时间为任务执行的时间
// scheduleWithFixedDelay 第二个任务执行的时间 = 第一个任务执行时间 + delay
// schedule 定时调度,延迟delay后执行,且只执行一次
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
// 流式(fork-join)线程池,将大任务拆分为很多小任务同时执行,执行完成后汇总结果,类似于MapReduce
public static ExecutorService newWorkStealingPool();

自己创建线程池

阿里的Java开发手册中强制线程池不允许使用Executors去创建,而应该通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

上面是JDK中最底层创建线程的构造方法,有7参数:

  • corePoolSize,核心线程数
  • maximumPoolSize,最大线程数
  • keepAliveTime,当线程池数量超过核心线程数时,多余的空闲线程存活的时间
  • unit,时间的单位,可以是毫秒、秒、分钟、小时和天等
  • workQueue,线程池中的线程数超过核心线程数时,任务将放在等待队列,它是一个BlockingQueue类型的对象
  • threadFactory,线程工厂,使用它来创建一个线程
  • handler,拒绝策略,当线程池和等待队列都满了之后的处理逻辑

这里着重看下workQueue、threadFactory和handler

workQueue

BlockingQueue类型的子类都可以作为等待队列,jdk默认提供的阻塞队列有:

1、ArrayBlockingQueue,基于数组实现的有界阻塞队列

2、LinkedBlockingQueue,基于链表实现的阻塞队列,可有界也可无界

3、SynchronousQueue,不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作将处于阻塞状态。是Executors.newCachedThreadPool()的默认实现

4、PriorityBlockingQueue,带优先级的无界阻塞队列

threadFactory

线程工厂,ThreadFactory是一个接口,下面是jdk中的定义

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

jdk提供了默认实现DefaultThreadFactory,主要用于创建一个线程名字为pool-{poolNum}-thread-{threadNum}的线程,下面是jdk中的实现

   /**
     * The default thread factory
     */
    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

如果要自定义线程池名字,值需要模仿jdk的实现来自己实现ThreadFactory接口即可

handler

拒绝策略,当线程池满了、队列也满了的时候,我们对任务采取的措施。或者丢弃、或者执行、或者其他...

jdk自带的4中拒绝策略:

  • CallerRunsPolicy // 在生成线程池的调用者线程执行
  • AbortPolicy // 抛出RejectedExecutionException异常
  • DiscardPolicy // 直接丢弃,不做任何处理
  • DiscardOldestPolicy // 丢弃队列里最旧的那个任务,再尝试执行当前任务

提交任务的两种方法execute()和submit()

主要区别在于:

execute()提交后无返回值,submit()有一个Future对象的返回值

通过调用Future对象的get ()方法,能拿到返回结果。get()会一直阻塞,直到拿到返回结果,当然也可以使用带超时时间的重载方法,在指定时间还没返回直接抛出TimeoutException。

关闭线程池

有两个关闭线程池的方法,shutdown()和shutdownNow()

shutdown()会将线程池状态设置为SHUTDOWN,不在接受新的任务,同时会等待线程池中已有的任务执行完成再结束。

shutdownNow()会将线程池状态设置为SHUTDOWN,对所有线程执行interrupt()操作,情况队列,并将队列中的任务返回回来。

关闭线程池涉及到两个返回boolean的方法,isShutdown()和isTerminated,分别为是否关闭和是否终止

配置正确的线程池参数

属于CPU密集型,那么可以将线程池数量设置成CPU的个数,来减少线程切换带来的开销。属于IO密集型,可以将线程池数量设置得更多一些,比如CPU个数*2。

通过Runtime.getRuntime().availableProcessors()来获取CPU的个数。

线程池监控

ThreadPoolExecutor自带的实现了的方法:

1、getTaskCount(),获取已经执行或正在执行的任务数
2、getCompletedTaskCount(),获取已经执行的任务数
3、getLargestPoolSize(),得到线程池曾经创建过的最大线程数,根据此参数,可以知道线程池是否满过
4、getPoolSize(),获取线程池线程数
5、getActiveCount(),获取活跃线程数,正在执行任务的线程数

ThreadPoolExecutor未实现的方法:

protected void beforeExecute(Thread t, Runnable r) { }

protected void afterExecute(Runnable r, Throwable t) { }

protected void terminated() { }

我们可以实现这些方法,然后打印出线程执行前,执行后和关闭时的一些信息。