Java并发编程的艺术之线程池的原理和使用

线程池的原理和使用

线程池有几个相关的类及其关系:

    ThreadPoolExecutor作为线程池的主要实现类,在线程池的创建和使用中都起到了很大的作用,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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

    1,核心线程数量:有的地方也叫线程池基本大小,是保留在线程中的线程数量,即使他们是空闲的,当一个任务提交到线程池中时,只要需要执行的线程数小于核心线程数机会创建线程。如果调用prestartAllCoreThreads()方法线程会提前创建并启动所有基本的线程。

    2,最大线程数量:就是允许创建的最大线程数量。

    3,keepAliveTime:空闲线程存活的时间,这个空闲线程指的是外包线程(核心线程之外的线程)。所以如果任务很多而且每个执行的时间较短,可以调大时间,这样避免再次创建线程,提高了线程利用率。

    4,任务队列:用于保存等待执行的任务的阻塞队列,有一下几种选择。

        ArrayBolckingQueue:基于数组有界,fifo

        LinkedBlockingQueue:基于链表,fifo,吞吐量比上一个高,静态工厂方法Executors.newFixedThreadPool使用它

        SychronousQueue:不存储元素的阻塞队列,每个插入必须等另一个移除,newCachedThreadPool使用它

        PriorityBlockingQueue:一个具有优先级的无限阻塞队列

    5, ThreadFactory:用于常见线程的工厂,可以设置共有意义的线程名

    6,饱和策略:队列和线程池都满了如何处理

一:线程池的实现原理

    ThreadPoolExecutor的execute(Runnable command)方法如下:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {    (1)
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) { (2)
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))    (3)
            reject(command);
    }

   线程池处理流程

    (1),判断运行线程是否小于核心线程数,如果是,则创建新线程来执行任务(此步骤需获取全局锁),直到核心线程池满了为止,如果满了,进行下一流程。

    (2),判断工作队列是否已满,如果没有,将新提交的任务存储在工作队列BlockingQueue里。如果工作队列满了,进入下一个流程。

    (3),判断最大线程数maximumPoolSize是否已满,如果没有,创建一个新的工作线程执行任务(此步骤需获取全局锁)。如果满了,交给饱和策略处理。

    工作线程所在的优先级顺序是:核心线程池 > 工作队列 > 最大线程池。

下面以图片的形式展示线程池的处理流程以及ThreadPoolExecutor的执行示意图

    ThreadPoolExecutor采用上述思路,为的是在执行esecute()方法时尽可能少的获取全局锁,在ThreadPoolExecutor完成预热之后(即当前运行的线程数大于corePoolSize),几乎所有的execute()方法都调用步骤2,而步骤2不需要获取全局锁。

    工作线程:线程池创建线程时,会将线程封装成工作现成Worker,Worker在执行完任务后,还会获取工作队列里的任务来执行。Worker类的run()方法。

    (1),execute()里的addWorker()方法创建一个线程,会让这个线程执行当前任务。

    (2),完成1中的任务后,会从BlockingQueue中获取任务来执行。

线程池创建

    线程池中使用Executors中的一系列静态方法例如:newFixedThreadPool(...),newSingleThreadPool(...),newCachedThreadPool(...)来创建线程,而这些方法在实现时都是依赖ThreadPoolExecutor的构造方法,只是构造方法里的参数不同而已。

    1,newFixedThreadPool 方法

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

    由代码可知,它的核心线程数和最大线程数相等,所以在完成预热后,他的线程个数是固定的,有了新的任务就塞进LinkedBlockingQueue里,等待工作线程空闲时执行任务。由于核心和最大线程数相等,所以不会有多余的线程,keepAliveTime这个参数也就没有什么意义了。

    这个方法的应用场景就是为了限制线程使用的资源,使用于负载比较重的服务器。缺点就是因为LinkedBlockingQueue的容量是Integer.MAX_VALUE,所以无法拒绝任务,也就是饱和策略RejectedExecutorHandler根本不会用到。

2,newSingleThreadExecutor 方法(注意他的名后缀是Executor而不是SingleThreadPool

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

    这个方法和之前的newFixedThreadPool类似,只不过是把固定的线程数换成只有一个线程,他的使用场景就是想要任务按照顺序执行;任意时间点,不会有多个线程是活动的场景。因为使用了无界的链表队列,所以也无法拒绝任务。

    

3,newCachedThreadPool 方法(需要理解这个Cached的含义,为什么这么起名?)

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

    这个方法和之前的两个区别很大,没有核心线程,全部都是外包线程,所以keepAliveTime可以对线程池的所有线程起作用,

默认的等待时间是60秒,但是他的队列是没有容量的(或者说是容量为1),这样如果主线程提交任务的速度大于线程中处理任务的速度,那么就会没有空闲线程,就会一直创建线程,造成CPU和内存大量消耗。

    这种线程池一般适用于很多的短期异步小程序(为什么是异步?因为它的队列没有容量,进来一个任务就可以立即执行),而且最好是不怕占用资源(也就是在负载较轻的服务器用)。

提交任务

    可以使用execute和submit方法向线程池提交任务,execute()方法不需要返回值,只需要提交一个Runnable类的实例就可以。而submit方法是在AbstractExecutorService就实现的方法,用于提交需要返回值的任务,线程池会返回一个Future类型的对象,通过这个对象来判断任务是否执行成功,可以通过Future的get()方法获取返回值,get方法会阻塞当前线程直到任务返回,当然也可以设置阻塞时间。

public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
Future<Object> future = executor.submit(harReturnValuetask);
try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池
executor.shutdown();
}

关闭线程池

    可以通过shutdown或shutdownNow来关闭线程池,他的原理是遍历线程池中的工作线程,然后逐个调用interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止,shutdown将线程池的状态设置SHUTDOWN状态,shutdownNow将线程池的状态设为STOP.

    只要调用了任意一个,isShutdown方法都会返回true,当所有的任务都关闭后,表示线程线程池关闭成功,这是isTerminaed方法会返回true。如果不一定要任务执行完可以调用shutdownNow。

参考:《Java并发编程的艺术》

猜你喜欢

转载自blog.csdn.net/fanxing1964/article/details/79488542