java(十二):线程池和ThreadPoolExecutor

来源:互联网 发布:windows dvd 播放器 编辑:程序博客网 时间:2024/05/21 09:06

阅读前请确保对于Thread和Runnable熟悉,可以参考多线程,Thread和Runnable
本博客从源码层面讲起,到最后常用的四个线程池。

Executor

这是线程池实现的最底层的接口,只包含一个方法:

public interface Executor {    /**     * Executes the given command at some time in the future.  The command     * may execute in a new thread, in a pooled thread, or in the calling     * thread, at the discretion of the {@code Executor} implementation.     */    void execute(Runnable command);}

可以看出,Executor是执行一个线程的(Runnable)的一个单元,即将Runnable放在Executor中执行。这意味着不需要在显式的声明一个Thread来执行一个线程了。
既然如此,那么重要的就在于如何实现execute方法了。

ExecutorService

接口,实现了Executor接口,另外提供了“停下”(shut down)的操作,并停止接受新的任务。

public interface ExecutorService extends Executor {    /**    * 官方解释是,停止接受新任务,已提交的任务则按序执行完毕,原话:    * allow previously submitted tasks to execute before terminating, but no new tasks will be accepted    */    void shutdown();    /**    * 即在shutdown上做的跟进一步,已提交的任务停止执行,返回等待的任务    * 并尝试停下当前在执行的任务,原话是:    * prevents waiting tasks from starting and attempts to stop currently executing tasks    * 注意"attempt",该方法并不能保证能停下正在执行的任务    */    List<Runnable> shutdownNow();    //提交一个任务,并返回该任务的Future类    Future<?> submit(Runnable task);    //执行给定的任务,所有执行完毕后,返回包含这些任务状态和结果的列表    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);    //执行给定的任务,任何一个执行完毕,则返回该执行完的任务的状态和结果,并取消的没执行完的任务    <T> T invokeAny(Collection<? extends Callable<T>> tasks)

AbstractExecutorService

抽象类,主要是提供了submit,invokeAny和invokeAll的具体实现。

public abstract class AbstractExecutorService implements ExecutorService {    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {        return new FutureTask<T>(runnable, value);    }    public Future<?> submit(Runnable task) {        if (task == null) throw new NullPointerException();        RunnableFuture<Void> ftask = newTaskFor(task, null);        execute(ftask);        return ftask;    }

submit方法,意为“提交”,和execute(执行)方法的区别在于,submit内部先将提交的任务包装成一个RunnableFuture,然后调用execute方法,并返回任务的结果。即submit可以取得任务的执行结果。
返回结果是通过RunnableFuture类实现的。即newTaskFor方法会将提交的任务包装成一个RunnableFuture并返回。
剩下的方法比如invokeAll和invokeAny等的实现都需要额外的类,具体实现就不贴了,没有什么很厉害的算法实现,要说的就是invokeAny的实现是借助ExecutorCompletionService来实现的。该类用于保证一个任务被执行完。具体等下个博客在分析吧。

ThreadPoolExecutor

最后来看看线程池ThreadPoolExecutor的实现,该类有2000多行,还是要花点耐心阅读的。
OK,一开始看看官方文档上对于ThreadPoolExecutor的具体介绍吧:

Thread pools address two different problems: they usually provide improved performance when executing large numbers of asynchronous tasks, due to reduced per-task invocation overhead, and they provide a means of bounding and managing the resources, including threads, consumed when executing a collection of tasks. Each ThreadPoolExecutor also maintains some basic statistics, such as the number of completed tasks.

翻译过来就是:
线程池主要解决了两个问题,1.当需要处理大量异步任务是,提供了更好的性能,原因是(线程池)降低了每个任务调用的消耗(相比使用Thread)2.提供了管理和绑定资源的手段;另外,线程池维护了一些基本的统计信息。
官方文档建议使用更容易的工厂方法,比如newCachedThreadPool,newFixedThreadPool,newSingleThreadExecutor,等来代替直接初始化,也就是我们常用到的四种线程池,这将在后文介绍。

线程池的状态和数量

介绍完该类的作用,直接看源码吧,首先说明线程池的5个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。
具体含义如下:
1. RUNNING:表示可以接受新任务,并处理队列的任务
2. SHUTDOWN:不允许接受新任务,但可以处理队列的任务
3. STOP:不允许接受新任务和处理队列的任务,并打断正在执行的任务
4. TIDYING:所有的任务执行完毕,workerCount为0,转变到TIDYING的线程会执行terminated()方法
5. TERMINATED:terminated()方法执行完毕
使用runState变量来表示。
以上5个状态时可以相互转换的,转换规则如下:
RUNNING -> SHUTDOWN:调用了shutdown()方法,而该方法一般是在finalize()里被调用的
(RUNNING or SHUTDOWN) -> STOP:调用了shutdownNow()方法
SHUTDOWN -> TIDYING:当任务队列为空并且线程池为空,则会发生这种状态的转变
TIDYING -> TERMINATED:terminated()方法执行完毕
需要说明的是,上述的状态转换是不能手动调的,当调用shutdown等方法或者出现其他异常时,这种状态的转换是会自动发生的。
另一个需要首先说明的变量是workerCount,该变量是记录有效线程数。
然后看下源码是如何表示上述两个变量的:

public class ThreadPoolExecutor extends AbstractExecutorService {    private static final int COUNT_BITS = Integer.SIZE - 3;    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;    // runState is stored in the high-order bits    private static final int RUNNING    = -1 << COUNT_BITS;    private static final int SHUTDOWN   =  0 << COUNT_BITS;    private static final int STOP       =  1 << COUNT_BITS;    private static final int TIDYING    =  2 << COUNT_BITS;    private static final int TERMINATED =  3 << COUNT_BITS;    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

上述代码的阅读比较困难,需要结合注释观看。
我们知道int在java中是4个字节,因为使用补码,因此,最大可赋予的正整数是2^31-1,这个数大概是20亿。也意味着如果使用int来记录有效线程数,那么最大的线程容量是20亿。
同样我们也可以使用int来表达线程池的5个状态。
但ThreadPoolExecutor的源码是并发的大佬Doug Lea写的,怎么会使用一般的方法呢><.
源码中的解释是,int可以用来表达20亿,但我们不需要这么多,从二进制上来说,我们可以使用低位的信息来保存线程的数量,高位保存线程池的状态。OK,那具体如何实现呢?
我们先看下上述变量的具体大小,Integer.SIZE=32,因此COUNT_BITS = 29,剩下的变量使用二进制表示如下:

RUNNING:    111|00000000000000000000000000000SHUTDOWN:   000|00000000000000000000000000000STOP:       001|00000000000000000000000000000TIDYING:    010|00000000000000000000000000000TERMINATED: 011|00000000000000000000000000000CAPACITY:   000|11111111111111111111111111111

仔细观察会发现,使用了整形的低29位表示线程的最大容量CAPACITY,即2^29-1.
剩下的高三位表示线程池的状态。
当我们需要改变状态时只需要改变高三位,改变线程数时,改变低29位即可。这就实现了将两个信息放在一个变量里面,即ctl。
然后使用逻辑运算,与或非等来取得各runState或者workerCount,源码中相应的方法如下:

public class ThreadPoolExecutor extends AbstractExecutorService {    ...    // Packing and unpacking ctl    private static int runStateOf(int c)     { return c & ~CAPACITY; }    private static int workerCountOf(int c)  { return c & CAPACITY; }    private static int ctlOf(int rs, int wc) { return rs | wc; }    /*     * Bit field accessors that don't require unpacking ctl.     * These depend on the bit layout and on workerCount being never negative.     */    private static boolean runStateLessThan(int c, int s) {        return c < s;    }    private static boolean runStateAtLeast(int c, int s) {        return c >= s;    }    private static boolean isRunning(int c) {        return c < SHUTDOWN;    }

上面的函数我们成为解包或者装包,即可以理解为解析ctl或者封装ctl。

线程池和队列

接着进入ThreadPoolExecutor中最重要的两个变量,线程池和任务队列。

public class ThreadPoolExecutor extends AbstractExecutorService {    ...    private final BlockingQueue<Runnable> workQueue;    private final HashSet<Worker> workers = new HashSet<Worker>();

其中,workQueue是任务队列,workers是线程池。
其实到这里,关于两者间的区别,还有很多疑惑,我们在这里详细讲解下两者的区别。
线程池中包含线程,线程是用来消费任务的,可以认为任务需要线程承接再能执行。
这里简述一种线程池可能的工作状态:

线程池为空,任务队列为空,此时开始有任务到来,没来一个任务创建一个线程用于承接任务,并执行,直到线程数到达a后,将到来的任务放在任务队列中,再次期间任务队列中的任务数增加,线程数不变,直到任务队列满了,然后再次开始创建线程用于消费新来的任务,直到线程数达到b。此时拒绝任务,即新来的任务被忽视掉。

上述描述大致明确了线程和队列的功能。
下面举个实际的例子看下:

public class Test01 {    public static void main(String[] args){        ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6,                200, TimeUnit.MILLISECONDS,                new ArrayBlockingQueue<Runnable>(4));        for(int i=0;i<10;i++){            MyTask myTask = new MyTask(i);            executor.execute(myTask);            System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+                    executor.getQueue().size());        }        executor.shutdown();    }}class MyTask implements Runnable {    private int taskNum;    public MyTask(int num) {        this.taskNum = num;    }    public void run() {        //System.out.println("正在执行task "+taskNum);        try {            Thread.currentThread().sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}//执行的结果如下://线程池中线程数目:1,队列中等待执行的任务数目:0//线程池中线程数目:2,队列中等待执行的任务数目:0//线程池中线程数目:3,队列中等待执行的任务数目:0//线程池中线程数目:3,队列中等待执行的任务数目:1//线程池中线程数目:3,队列中等待执行的任务数目:2//线程池中线程数目:3,队列中等待执行的任务数目:3//线程池中线程数目:3,队列中等待执行的任务数目:4//线程池中线程数目:4,队列中等待执行的任务数目:4//线程池中线程数目:5,队列中等待执行的任务数目:4//线程池中线程数目:6,队列中等待执行的任务数目:4

从结果中可以看出首先是线程增加,然后是队列中的任务数增加,队列满后,再次增加线程数。
再次验证如下:

public class Test01 {    public static void main(String[] args){        ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6,                200, TimeUnit.MILLISECONDS,                new ArrayBlockingQueue<Runnable>(4));        for(int i=0;i<10;i++){            MyTask myTask = new MyTask(i);            executor.execute(myTask);//            System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+//                    executor.getQueue().size());        }        executor.shutdown();    }}class MyTask implements Runnable {    private int taskNum;    public MyTask(int num) {        this.taskNum = num;    }    public void run() {        System.out.println("正在执行task "+taskNum);        try {            Thread.currentThread().sleep(2000);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}//一种可能的执行结果://正在执行task 0//正在执行task 8//正在执行task 7//正在执行task 1//正在执行task 9//正在执行task 2//正在执行task 3//正在执行task 4//正在执行task 5//正在执行task 6

上述只是一种可能的结果,但无论随机性多大,最后四条输出任务绝对是3,4,5,6,这是因为这四条是任务队列中的,只有等线程空闲了才能执行任务队列中的任务。
两个变量的讲述暂时到此为止

补充:锁

源码中还有一行代码:

public class ThreadPoolExecutor extends AbstractExecutorService {    ...    private final ReentrantLock mainLock = new ReentrantLock();

源码解释,任何对于workers线程池的操作都需要获得mainLock 锁。
为什么不使用synchronized同步方法呢?
有两点原因:1.使用锁更灵活,而synchronized消耗较多资源2.synchronized是非公平锁,即不是先来先得(FIFO)锁,因此可能产生大量的“打断”(interrupt)操作。
因此我们使用显式的锁机制。

其他变量

public class ThreadPoolExecutor extends AbstractExecutorService {    ...    private volatile ThreadFactory threadFactory;    private volatile RejectedExecutionHandler handler;    private volatile long keepAliveTime;    private volatile boolean allowCoreThreadTimeOut;    private volatile int corePoolSize;    private volatile int maximumPoolSize;

虽然是其他变量,但这些变量多是我们能初始化的变量。
threadFactory:线程工厂,用于创建线程
handler:拒绝处理方法
keepAliveTime:多出的线程的等待时间
allowCoreThreadTimeOut:是否允许核心线程也按照keepAliveTime而消亡
corePoolSize:核心线程数
maximumPoolSize:最大线程数
理解了以上参数,就不难理解线程池的工作机制了。

源码解析暂时到此为止,等有时间了在

四个线程池

四个线程池都是通过Executors里的静态方法创建的,四个方法如下:
–newCachedThreadPool()

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

需要注意的是SynchronousQueue,SynchronousQueue内部没有数据缓存空间,这意味着可以认为SynchronousQueue长度也为0.
于是newCachedThreadPool返回的线程池的功能就很明确了:

来一个新任务,如果没有空闲的线程来承载该任务,就创建线程,否则用空闲的线程去承载新任务。如果空闲的线程超过60s没有接待新任务,则被销毁,知道线程数为0.

该线程池的特点是会立刻满足每一个任务,不让任何任务等待。

–newFixedThreadPool(n)

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

注意到初始化参数,该防范创建的最大线程数和核心线程数相等,并且使用链表队列,这意味着:

一开始会创建n个线程用于承载任务,任务数超过n后,则将任务塞在链表队列中,链表队列无限增长

–newScheduledThreadPool(n)
创建一个定长线程池,并且支持定时和周期性的执行任务.
这个就不看源码了,直接举例吧:

/** * 定时执行 */  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(20);  // 长度20  // 延迟10s执行scheduledThreadPool.schedule(task, 10, TimeUnit.SECONDS);  /** *周期性执行 */  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(20);  // 长度20  // 延迟10s执行,每个5s执行一次。scheduledThreadPool.scheduleAtFixedRate(task,10, 5, TimeUnit.SECONDS); 

–newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行

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

到这里,主要的四个线程池就讲解完毕了。
至于ThreadPoolExecutor的核心实现等有时间在总结
最后按照惯例,贴一下参考到的资料:
深入理解java之线程池
java四种线程池的使用

原创粉丝点击