快速了解Java线程池

本文同时载于本人CSDN

1.概述

使用Thread类执行某个任务,任务运行时每次都要创建线程,任务结束则要销毁线程,对系统而言,线程不仅是资源,线程的创建与销毁也要消耗系统的资源。对于此问题,很显然一个直接的解决方案就是复用这个线程,让线程执行完某个任务后仍然能继续执行执行其它任务,而不是销毁。线程池就提供了这样的解决方案。

线程池指的是存在一组线程,这组线程创建后一般不会销毁,而是进入休眠状态,等到有新的任务需要执行又重新恢执行。

2.创建一个线程池

java.util.concurrent(JUC)包提供了很多接口用于线程池的创建。

2.1 ExecutorService 接口

线程池通常指的是ExecutorService —— 一个提供了终止管理的方法和可以生成Future的方法的Executor。简单地说,ExecutorService接口继承了Executor接口,ExecutorService除了存在Executor提供的execute方法外,还提供了很多终止管理的方法以及产生用于跟踪线程执行结果的Future的submit。如下所示


boolean awaitTermination(long timeout, TimeUnit unit)

 List> invokeAll(Collection> tasks)

 List> invokeAll(Collection> tasks, long timeout, TimeUnit unit)

 T   invokeAny(Collection> tasks)

 T   invokeAny(Collection> tasks, long timeout, TimeUnit unit)

boolean isShutdown()

boolean isTerminated()

void    shutdown()

List  shutdownNow()

 Future   submit(Callable task)

Future   submit(Runnable task)

 Future   submit(Runnable task, T result)

2.2 直接通过 ThreadPoolExecutor创建线程池

JUC提供了ThreadPoolExecutor类用于创建线程池,这个类继承了实现ExecutorService接口的AbstractThreadPoolExecutor。ThreadPoolExecutor有若干个构造参数,一般可以通过调用如下所示的创建一个线程池:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)

代码如下

ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new BlockingArrayQueue(40));

其中,各个参数的含义如下:

  • corePoolSize 线程池中保持存活的线程数量,即使线程是空闲的,除非调用allowCoreThreadTimeOut(true)
  • corePoolSize 线程池中最大能创建的线程数量
  • keepAliveTime 空闲线程的存活时间
  • unit KeepAliveTime参数的单位例如 TimeUnit.SECONDES等
  • workQueue 一个储存所提交的runnable的任务的队列,只会execute方法提交的runnable任务

通过上面的参数描述,我们可以知道,通过ThreadPoolExecutor创建的的线程池是可以自动调整大小的。当一个新的任务提交时,如果此时线程池中的线程小于corePoolSize,那么线程池就会创建一个新的线程来执行此任务,即使其它的线程处于空闲状态。如果此时线程池中存在大于corePoolSize但是小于maximumPoolSize的线程数,除非workQueue是满的,否则不会创建新的线程。因此,我们可以通过设置corePoolSizemaximumPoolSize相等来创建一个固定线程数的线程池,也可以通过设置一个实质上没有边界的的值例如Integer.MAX_VALUE来创建可以容纳任意并发线程数的线程池。一般来说,corePoolSizemaximumPoolSize在线程池构造时就已经确定,但是我们也可以通过 setCorePoolSize(int)setMaximumPoolSize(int)动态设置这两个值。

2.3 线程池中线程的创建

线程池使用ThreadFactory创建新线程。如果没有另外指定,则使用Executors.defaultThreadFactory(),它将所有线程创建在同一个ThreadGroup中,并具有相同的NORM_PRIORITY优先级和非守护进程状态。 通过提供不同的ThreadFactory,我们可以更改线程的名称,线程组,优先级,守护程序状态等。如果ThreadFactory调用newThread时返回null创建线程失败,则executor将继续,但可能无法执行任何任务。 线程应该拥有“modifyThread”的RuntimePermission。如果使用线程池的工作线程或其他线程不具有此权限,则服务可能会降级:配置更改可能不会及时生效,并且关闭线程池可能保持可以终止但未完成的状态。

2.4 线程存活时间

如果池当前具有多于corePoolSize的线程,则多余线程如果空闲时间超过keepAliveTime,则将终止(请参阅getKeepAliveTime(TimeUnit))。 这提供了一种在不使用池时减少资源消耗的方法。如果线程池稍后变得更活跃,则将构造新线程。 也可以使用方法setKeepAliveTime(long,TimeUnit)动态更改此参数。使用Long.MAX_VALUE TimeUnit.NANOSECONDS这样的值将有效地禁止空闲线程在线程池关闭之前终止。 默认情况下,仅当存在大于corePoolSize的线程时,保持活动策略才适用。但是方法allowCoreThreadTimeOut(boolean)也可用于将此超时策略应用于核心线程,只要keepAliveTime值不为零。

2.5 工作队列

任何BlockingQueue都可用于传输和保存提交的任务。此队列的使用与池大小相互影响:

  • 如果运行的线程少于corePoolSize,则Executor总是更喜欢添加新线程而不是将任务加入队列。
  • 如果corePoolSize或更多线程正在运行,则Executor总是更喜欢排队请求而不是添加新线程。
  • 如果队列已经满了,则会创建一个新线程,除非它超过maximumPoolSize,在这种情况下,该任务将被拒绝。

队列选择有三种常规策略:

  • 直接交接。工作队列的一个很好的默认选择是SynchronousQueue,它将任务交给线程而不另外保存它们。如果没有线程立即可用于执行任务,则尝试添加一个新的任务将会失败,因此将构造新线程。此策略在处理可能具有内部依赖性的请求集时避免了锁定。直接交接法通常需要无边界的maximumPoolSizes以避免拒绝新提交的任务。这也就意味着,当任务以比处理它们更快的速度到达时,线程可能会无限制增长。
  • 无限队列。使用无限制队列(例如,没有预定义容量的LinkedBlockingQueue)将导致新任务在没有空闲线程时在队列中等待。因此,线程池只会创建corePoolSize数量的线程(maximumPoolSize的值因此没有任何效果)。当每个任务完全独立于其他任务时,这可能是适当的,因此任务不会影响彼此的执行;例如,在网页服务器中。虽然这种排队方式可以有助于平滑瞬态突发请求,但是这同时意味着当任务继续平均到达的速度超过可处理速度时,工作队列可能会无限制增长。
  • 有限的队列。有限队列(例如,ArrayBlockingQueue)与有限maximumPoolSizes一起使用时有助于防止资源耗尽,但可能更难以调整和控制。队列大小和最大池大小之间可以相互折中(trade off):使用大型队列和小型池可以最小化CPU使用率,OS资源和上下文切换开销,但可能导致人为的低吞吐量。如果任务经常阻塞(例如,如果它们执行时存在I/O操作),则系统可能能够提供比maximumPoolSizes更多的线程调度时间。使用小队列通常需要更大的池大小,这会使CPU更加繁忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。

2.6 任务提交

提交任务有两种方法,一种是提交无返回值的任务,使用如下方式:

ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new BlockingArrayQueue(40));
Runnable handler = ......;
executor.execute(handler);

如果需要获得任务的执行结果,则可以通过调用submit方法来提交任务,代码如下:

ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new BlockingArrayQueue(40));
Callable handler = ......;
Future future = executor.submit(handler);

2.7 创建线程池的完整实例

import java.util.concurrent.*;

public TreadPoolTest(){
    public static void main(String[] args){
        ExecutorService executor = new TreadPoolExecutor(5, 10, 10L,TimeUnit.SECONDS,new ArrayBlockingQueue(40));
        for(int i = 0;i < 10; i++){
            Handler handler = new Handler();
            executor.execute(handler);
        }
        executor.shutdown();

    }

    class Handler extends Runnable{
        public void run(){
            for(int i = 0; i < 5;i++){
                System.out.println(i);
            }
        }
    }
}

3 通过Executor的工厂方法创建线程池

Executors是JUC包下的一个工厂类,他提供了许多便捷的方式创建线程池。

3.1 创建固定大小的线程池

可以通过Executors类创建一个固定大小的线程池:

ExecutorService executor = Executors.newFixedThreadPool(10);

我们前面已经说过,固定大小的线程池可以通过设置ThreadPoolExecutor 核心线程数和最大线程数相等实现,事实上源码也是如此的:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue());
    }

一般来说,当线程数达到核心线程数,既不会再创建线程,也不会销毁线程,除非发生异常某个线程终止,或者设置allowCoreTimeOut,但最好别这么做,因为keepAlive参数此时被设置为0,可能会导致频繁的创建与销毁线程(这里仅仅是推测。仍需验证)。

3.2 创建可缓存的线程池:

可以通过Executors.newCachedThreadPool 创建一个可缓存的线程池,如果当前线程池的规模超出了处理需求,将回收空的线程;当需求增加时,会增加线程数量;线程池规模无限制。代码如下:

ExecutorService executor = Executors.newCachedThreadPool(10);

前面同样也提到,可以设置maxmumPoolSize参数为一个实质上无边界的值如Integer.MAX_VALUE让线程池的规模无限制,实际上源码也是如此实现的:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
        60L, TimeUnit.SECONDS,
        SynchronousQueue());
    }

3.3 创建单线程的线程池

可以通过Executors.newSingleThreadExecutor() 创建一个单线程的Executor,确保任务串行执行:

ExecutorService executor = Executors.newSingleExecutor();
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
            0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue()));
    }

4 总结

这篇文章深入描述线程池的创建方法,各个参数的意义与作用,但是线程池除以上所描述的,还存在可调度的线程池,在本系列的下一篇文章将讲述这一类线程池。

你可能感兴趣的:(快速了解Java线程池)