逐渐深入Java多线程(一)----Java线程池和Executor框架说明

目录

 

从Executor说起

ThreadPoolExecutor

ThreadPoolExecutor的Worker

ThreadPoolExecutor新增任务时的处理流程

ThreadPoolExecutor的线程池状态

Executors类

1,单线程处理的线程池,newSingleThreadExecutor

2,控制最大并发的线程池,newFixedThreadPool

3,可回收缓存线程池,newCachedThreadPool

4,延时或周期执行任务的线程池,newScheduledThreadPool

5,支持定时任务的单线程线程池,newSingleThreadScheduledExecutor

6,工作窃取式线程池,newWorkStealingPool


从Executor说起

从JDK1.5开始,为了处理多线程和线程池的问题,java引入了Executor框架。线程池的使用可以减少线程不断创建和撤销造成的资源损失。

在java的多线程框架中,有工作单元和执行机制的概念,工作单元指的是Runnable和Callable接口,代表任务本身,执行机制指的是Executor框架,负责任务的处理机制。

 

Executor接口是Executor框架中顶层的接口,只有一个方法:

void execute(Runnable command);

方法的功能是把任务添加到线程中并开始执行。

 

Executor框架的类关系图:

ExecutorService是Executor接口的子接口,提供了很多用于处理线程的方法,可以用来管理线程的生命周期,比如:

1,void shutdown();

关闭连接池,连接池中已有的任务会继续执行,但是不能再向连接池中添加新的任务了。

注:当调用这个接口后,线程池就会变成关闭状态,调用isShutdown()方法返回的会是true,即使线程池中还有剩余任务正在执行。

2,List<Runnable> shutdownNow();

尝试中断当前正在执行的任务,中止等待状态的任务,并且返回还没开始执行的任务的列表。

3,boolean isShutdown();

判断连接池是否已经被关闭。

注:这个方法返回true时不代表线程池中的任务都已经执行完成。

4,boolean isTerminated();

判断连接池关闭后,线程池中的任务是否都已经执行完毕。

注:此方法应该在线程池关闭后使用,如果线程池没关闭,此方法永远返回false。

5,boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

阻塞当前线程,直到线程池关闭且任务都执行完成,或者到达超时时间,或者当前线程被中断。

注:调用此方法前最好先把线程池关闭。因为要解除此方法的阻塞,需要线程池关闭而且任务都执行完成,所以,如果调用此方法前没有把线程池关闭,那基本就没机会再把线程池关闭了,只能等超时了(或者interrupt)。

6,Future<T> submit(Callable<T> task);

提交一个Callable的任务,并把返回值放入Future中。

调用submit方法后,可以用Future的get()方法获得Callable的返回值,此时如果任务没执行完,get()方法会阻塞当前线程。

7,Future<T> submit(Runnable task)

提交一个Runnable的任务。

Runnable的run()方法没有返回值,但是我们依然可以得到Future返回值,和submit(Callable)不同,submit(Runnable)后调用Future的get()方法不会阻塞当前线程,当然也不会有值,一直都是null,不过我们可以通过Future的isDone()方法来判断线程是否执行完成。

8,Future<T> submit(Runnable task, T result);

提交一个Runnable的任务。和上面方法的不同就是多了一个T result,这个result会在任务执行完成后被放入Future,然后调用Future的get()方法就可以得到此值,另外,在此场景下的get()方法会阻塞线程。

9,List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

提交多个任务,并按照连接池的设定开始执行,任务的返回值放入List对应位置的Future中,任务完成前方法会阻塞。

10,List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                              long timeout, TimeUnit unit)
    throws InterruptedException;

提交多个任务,并设置任务的超时时间,任务的返回值放入List对应位置的Future中,任务完成前方法会阻塞。

注:此处的超时时间是针对整个连接池的,不是针对单个任务的,也就是说,从连接池开始执行任务,到超时时间为止,没完成和没开始执行的任务都会被中止。

11,<T> T invokeAny(Collection<? extends Callable<T>> tasks)
    throws InterruptedException, ExecutionException;

提交多个任务,在完成第一个任务后,返回结果,并把其他任务用interrupt()方法全部中断。

注:线程运行过程中因为发生异常而结束的,不算完成任务,如果所有线程都因发生异常而结束,则方法返回最后一个异常。

12,<T> T invokeAny(Collection<? extends Callable<T>> tasks,
                long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;

和前面的invokeAny()方法相同,不过增加了超时的设定。

 

ThreadPoolExecutor

ThreadPoolExecutor是创建并处理线程池的类,尽量使用这个类来创建线程池,自己定义合适的线程池参数,避免直接用Executor接口和Executors工具类,他们会导致无限长的任务队列(比如newSingleThreadExecutor),或者导致无限多的线程(比如newCachedThreadPool),存在内存溢出的风险。

 

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;
}

构造方法的几个参数说明如下:

1,int corePoolSize

线程池中核心线程数。活动线程未达到此值时,新的任务会新建Worker线程来处理。如果要执行的任务多于此值,多出来的任务会被放进workQueue任务队列或扩展线程上限至maximumPoolSize后继续新建Worker线程来处理。

注:当corePoolSize未满时,新加的任务一定会分配新的线程来处理,即使有Worker空闲,所以newCachedThreadPool把这个值设为0,使得新加的任务优先考虑空闲Worker,没有空闲Worker才会新建线程。

2,int maximumPoolSize

线程池中的最大线程数。

3,long keepAliveTime

空闲线程销毁时间。当线程池中线程数大于corePoolSize时,多出来的线程会在此时间后被销毁。

4,TimeUnit unit

线程销毁时间的时间单位。TimeUnit是个枚举,提供的时间单位有:天,小时,分钟,秒,毫秒,微秒,纳秒。(这个纳秒是什么场合下用的?)

5,BlockingQueue<Runnable> workQueue

任务队列。当任务数小于corePoolSize时,任务被添加到线程池分配线程来处理,而当corePoolSize已满时,任务就放在这个队列里。

BlockingQueue是在java.util.concurrent包下的一个接口类,这个接口有以下几个实现类:

  • ArrayBlockingQueue
  • DelayQueue
  • LinkedBlockingDeque
  • LinkedBlockingQueue
  • LinkedTransferQueue
  • PriorityBlockingQueue
  • SynchronousQueue

分别适用于不同的场景。

6,ThreadFactory threadFactory

创建新线程用的工厂类,ThreadFactory接口只有一个方法:

Thread newThread(Runnable r);

另外,这个工厂类有默认实现,可以通过:

Executors.defaultThreadFactory();

来获取。

7,RejectedExecutionHandler handler

拒绝策略。任务太多来不及处理时的处理策略。

这个参数也有默认值,就是ThreadPoolExecutor类中的静态变量:

private static final RejectedExecutionHandler defaultHandler =new AbortPolicy();

AbortPolicy是ThreadPoolExecutor类中的一个内部类,代码如下:

public static class AbortPolicy implements RejectedExecutionHandler {
    /**
     * Creates an {@code AbortPolicy}.
     */
    public AbortPolicy() { }

    /**
     * Always throws RejectedExecutionException.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     * @throws RejectedExecutionException always
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

可见,这个类拒绝新线程的办法是抛出了一个RejectedExecutionException异常。

 

ThreadPoolExecutor提供了以下几种不同的拒绝策略:

1,CallerRunsPolicy。在调用者线程中直接执行此任务,前提是线程池还未关闭。

2,DiscardPolicy。开心愉快的把要添加的任务忽略掉。显然此策略会导致任务丢失。

3,DiscardOldestPolicy。丢弃队列中最老的任务,也就是最早被添加到队列里的那个任务,也就是下一个要执行的任务,然后重试提交任务。

4,AbortPolicy。抛异常。此为默认策略。

我们也可以自己定义拒绝策略,实现RejectedExecutionHandler接口并重写

void rejectedExecution(Runnable r, ThreadPoolExecutor executor);

接口即可。

 

ThreadPoolExecutor的Worker

Worker是在ThreadPoolExecutor中定义的一个内部类,用于处理线程池中的任务。

Worker类维护了两个参数:

一个是要执行的任务firstTask,这个参数起名叫firstTask是有原因的,每个Worker初始化的时候都会同时分配一个任务并赋值给firstTask,就是所谓的第一个任务,而当Worker完成这个任务后又会调用getTask()方法从BlockingQueue中获取新的任务并执行。

第二个是Worker对象专用的线程,每个Workder对象都有属于自己的线程。Worker的线程启动时,同时会调用Worker的run()方法。

可见,Worker实际上对要执行的任务进行了封装,之所以使用Worker来封装和处理任务而不是直接调用任务的run()方法,是因为Worker使用了一种锁机制,使正在执行中的任务不会轻易被中断。

当Worker完成当前任务后,会从BlockingQueue中获取新的任务执行,如果BlockingQueue中没有任务,则Worker退出。

 

ThreadPoolExecutor新增任务时的处理流程

当执行ThreadPoolExecutor.execute()或者ThreadPoolExecutor.sumit()时,大概的处理逻辑如下:

1,判断当前Worker数(也就是线程数)是否小于corePoolSize,如果小於则新增Worker并处理该任务。这一步会让线程数大于0而小于等于corePoolSize。

2,如果当前线程数已经大于corePoolSize,则尝试把任务放入BlockingQueue。如果放入成功,则在BlockingQueue中等待Worker处理。

3,如果放入BlockingQueue失败则直接新建Worker并处理该任务,只要线程数没达到maximumPoolSize。这一步和第一步很像,区别只是这一步的逻辑可能会让线程数大于corePoolSize而小于maximumPoolSize。另外,只有corePoolSize已满且无法放入BlockingQueue时才会来到这一步。

4,如果放入BlockingQueue失败而且maximumPoolSize已满,则执行拒绝策略。

 

ThreadPoolExecutor的线程池状态

ThreadPoolExecutor的线程池有以下几种状态:

1,RUNNING。

处于此状态下的线程池,可接受新任务,可继续执行池中未完成的任务。

2,SHUTDOWN。

处于此状态下的线程池,不能接受新任务,但是可以继续执行池中未完成的任务。

调用shutdown()方法或finalize()方法,会让线程池进入此状态。

3,STOP

处于此状态下的线程池,不能接受新任务,也不会继续执行池中未完成的任务,当前正在执行的任务会发送中断指令(Interrupt)。

在RUNNING或者SHUTDOWN状态下调用shutdownNow()方法,会让线程池进入此状态。

4,TIDYING

进入此状态的连接池会触发terminated()钩子,调用terminated()方法。

当线程池中所有任务都完成,而且所有Worker线程都关闭后,线程池会进入此状态。

5,TERMINATED

当terminated()方法调用结束后,线程池进入此状态。

 

这些状态之间的转换关系如下图:

图2

 

Executors类

Executors和Executor的不同,就像Collections和Collection的不同一样,Executor是接口,而Executors是相关的工具类,提供了很多创建线程池的方法。

比如这个方法:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

作用是创建单一处理线程的线程池。

 

Executors可以创建以下几种不同的线程池,分别是:

1,单线程处理的线程池,newSingleThreadExecutor

Executor创建单线程处理的线程池,提供了两个方法:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

返回的是一个FinalizableDelegatedExecutorService类,这是Executors类中自己定义的一个类,提供了对ThreadPoolExecutor类的一个简单封装。

这个ThreadPoolExecutor,corePoolSize为1,maximumPoolSize为1,keepAliveTime为0,队列为LinkedBlockingQueue,长度不限。

参照ThreadPoolExecutor提交任务时的处理逻辑可以看出此线程池的特点:只可能生成一个Worker线程来处理任务,但是可以向队列中无限添加任务,并由这个Worker一个一个处理。

要小心因为队列中任务过多而导致的内存溢出。

和直接返回ThreadPoolExecutor相比,FinalizableDelegatedExecutorService类重写了finalize()方法:

static class FinalizableDelegatedExecutorService
    extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
        super(executor);
    }
    protected void finalize() {
        super.shutdown();
    }
}

finalize()的内容是把线程池关闭,而finalize()方法会被垃圾回收机制调用,也就是说,即使我们自己不调用shutdown()方法,垃圾回收器也会帮我们把线程池关闭。然而,作为负责任的开发,不能依靠finalize()方法,因为java从来不保证finalize()方法什么时候调用,甚至调不调用,所以,请手动关闭连接池。

 

2,控制最大并发的线程池,newFixedThreadPool

Executor创建控制最大并发的线程池,提供了两个方法:

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

这个ThreadPoolExecutor,corePoolSize为nThreads,maximumPoolSize为nThreads,keepAliveTime为0,队列为LinkedBlockingQueue,长度不限。

参照ThreadPoolExecutor提交任务时的处理逻辑可以看出此线程池的特点:最多可以生成nThreads个Worker线程来处理任务,但是可以向队列中无限添加任务,并由这个Worker一个一个处理。

要小心因为队列中任务过多而导致的内存溢出。

 

3,可回收缓存线程池,newCachedThreadPool

Executor的连接池本来就是可回收的,Worker线程完成当前任务后就会从线程池中继续获得任务并执行,但是,线程池中的活动线程数小于corePoolSize时会直接新建线程,而不是使用已有线程,所以这个所谓可回收缓存线程池,只不过是把corePoolSize设成了0。

Executor创建可回收缓存线程池,提供了两个方法:

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

这个ThreadPoolExecutor,corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,keepAliveTime为60秒,队列为SynchronousQueue。

参照ThreadPoolExecutor提交任务时的处理逻辑可以看出此线程池的特点:

1.)因为corePoolSize为0,所以不会生成Worker添加到corePoolSize核心线程池中。

2.)因为任务队列是SynchronousQueue,所以,如果没有空闲Worker正在等待时,无法把任务添加到BlockingQueue中,只能直接进行第三步,生成新Worker。因为maximumPoolSize为Integer.MAX_VALUE,所以生成的Worker数可以达到Integer.MAX_VALUE这么多。要小心因为Worker数过多而导致的内存溢出。

3.)所以当我们向这个连接池添加任务时只可能出现两种情形:要么没有空闲Worker,然后生成新的Worker线程来处理,要么恰好有空闲Worker,让空闲Worker来处理,这也就是所谓可回收和缓存的原理。

 

4,延时或周期执行任务的线程池,newScheduledThreadPool

Executor创建定时任务线程池,提供了两个方法:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

返回的是一个ScheduledThreadPoolExecutor,参数中的corePoolSize会成为ScheduledThreadPoolExecutor的corePoolSize。

其中ScheduledThreadPoolExecutor()构造的代码:

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

ScheduledThreadPoolExecutor的父类就是ThreadPoolExecutor,所以这个线程池返回的实际上也是一个ThreadPoolExecutor,其corePoolSize自定义,maximumPoolSize为Integer.MAX_VALUE,BlockingQueue为DelayedWorkQueue。

由参数可以看出这个线程池的特点:

1,核心线程数自定义,最大线程数可以很大。

2,BlockingQueue用的是DelayedWorkQueue,这是以一种有优先级的队列,基于堆排序,按照任务的延时时间进行排序。

所以我们可以用:

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

方法延时执行任务。

可以用:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                     TimeUnit unit);

方法不断循环执行线程池中的任务,此方法设定任务完成后到下次执行的延时时间。

可以用:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit);

方法循环执行线程池中的任务,此方法设定任务执行开始到下次执行的延时时间,但是如果延时时间到了上次任务还没执行完,则会等待任务执行完后立即开始。

 

5,支持定时任务的单线程线程池,newSingleThreadScheduledExecutor

Executor支持定时任务的单线程线程池,提供了两个方法:

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}
public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1, threadFactory));
}

二者的区别是一个ThreadFactory参数。

返回的是一个DelegatedScheduledExecutorService,提供了一个对ScheduledThreadPoolExecutor的封装。

这个线程池和ScheduledThreadPoolExecutor差不多,可以重复执行线程池中的任务,可以配置从任务开始时间开始的延迟,或从任务结束时间开始的延迟,用的方法也一样,只不过这是一个corePoolSize为1的线程池,只会有一个线程来处理任务,如果一个任务调用Thread.sleep(),所有的任务都会暂停。

 

6,工作窃取式线程池,newWorkStealingPool

JDK1.8才加入的线程池类型,使用的是ForkJoinPool,ForkJoinPool是JDK1.7加入的类,父类是AbstractExecutorService。这种线程池会创建足够多个线程来处理任务,充分利用多核CPU的性能,不保证任务执行的顺序。

Executor创建newWorkStealingPool,提供了两个方法:

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}
public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

返回的是一个ForkJoinPool。这个线程池的特点也是由ForkJoinPool决定的。

 

以上

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章