线程池任务提交原理,阻塞任务队列与拒绝策略

ThreadPoolExecutor参数解析

之前学习线程池,发现线程池大致有四种创建方法:
线程池任务提交原理,阻塞任务队列与拒绝策略_第1张图片

  1. newFixedThreadPool 创建一个指定大小的线程池
  2. newCachedThreadPool 创建一个可缓冲的线程池
  3. newSingleThreadExecutor 创建一个仅有一个线程的线程池。
  4. newScheduledThreadPool 创建一个可周期性调度任务的线程池
	public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

观察他们的源码可以发现底层都是调用的 ThreadPoolExecutor。
阿里巴巴的 java 开发规范不建议使用这四种线程池创建方式,因为第二个参数最大线程数为 Integer.MAX_VALUE,或者任务队列无上界,造成大量的任务或大量的线程,从而造成 OOM。

比如 newFixedThreadPool 与 newSingleThreadExecutor 的阻塞队列容量都是 Integer.MAX_VALUE。而 newCachedThreadPool 与 newScheduledThreadPool 的最大线程数量都是 Integer.MAX_VALUE , 这些都会造成巨大的问题。
强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。

下面主要解析ThreadPoolExecutor的创建方式
在这里插入图片描述参数介绍:

  1. corePoolSize 核心线程数:

    核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。
    核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。

  2. maximumPoolSize 最大线程数

    当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会启用拒绝策略。

  3. keepAliveTime 空闲线程的存活时间

    当线程数量大于corePoolSize核心线程数,并有线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。

  4. unit

    keepAliveTime的时间单位
    线程池任务提交原理,阻塞任务队列与拒绝策略_第2张图片

  5. workQueue

    任务队列,当没有空闲线程池的时候,将任务添加到任务队列中,等待线程空闲再去执行。
    线程池任务提交原理,阻塞任务队列与拒绝策略_第3张图片

  6. threadFactory

    线程工厂,用于配置创建线程方式。如更改线程池中创建的线程名字等。

  7. handler

    线程池对拒绝任务的处理策略,当线程数量达到最大线程数量并且任务队列已满,就会执行拒绝策略。
    线程池任务提交原理,阻塞任务队列与拒绝策略_第4张图片

注意:
线程池有一个属性private volatile boolean allowCoreThreadTimeOut;用于控制核心线程空闲时间会否会超时。
可用这个方法在线程池创建之后,使用之前来设置。默认为false.

pool.allowCoreThreadTimeOut(boolean value);
设置控制核心线程是否会超时和终止的策略,如果在keep-alive时间内没有任务到达,则在新任务到达时替换。当为false时,核心线程永远不会终止,因为缺少传入的任务。当为true时,同样适用于非核心线程的keep-alive策略也适用于核心线程。为了避免持续的线程替换,当设置为true时,keep-alive时间必须大于零。通常应该在池被积极使用之前调用此方法。

测试实例

通过具体的例子,来分析一下这些参数,以及查看一下线程池中任务提交流程。

查看线程池线程创建,任务队列,拒绝策略

import jdk.nashorn.internal.runtime.regexp.JoniRegExp;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *
 * @author xiaogang.zhang
 */
public class ThreadPoolExecutorTest {
    public static class RunTask implements Runnable {

        private String taskName;

        public RunTask(String taskName) {
            this.taskName = taskName;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " [" + this.taskName + "] " + " running!");
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " [" + this.taskName + "] " + " is over!");
        }
    }

    public static void main(String[] args) {

        //创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(4, 6, 20,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadFactory() {

            final private AtomicInteger atomicInteger = new AtomicInteger();

            @Override
            public Thread newThread(Runnable r) {
                //以原子方式将当前值加 1。
                int index = atomicInteger.incrementAndGet();
                System.out.println("create Thread: " + "Thread-" + index);
                return new Thread(r, "Thread-" + index);
            }
        }, new ThreadPoolExecutor.AbortPolicy());
		
		//设置核心线程永不过时
        pool.allowCoreThreadTimeOut(false);		

        for (int i = 1; i <= 20; i++) {
            pool.submit(new RunTask("task-" + i));
        }
	
		//关闭线程池
        pool.shutdown();
    }
}

上面的代码设定了线程池的参数:

核心线程数=4,
最大线程数=6,
空闲线程存活时间为20s,
任务队列为ArrayBlockingQueue容量为2,有界的阻塞数组
使用线程工厂来观测线程创建时机以及指定线程的名称。
拒绝策略为ThreadPoolExecutor.AbortPolicy(),处理程序会抛出一个RejectedExecutionException异常。
提价了20个任务

查看运行结果:
线程池任务提交原理,阻塞任务队列与拒绝策略_第5张图片
从运行结果中可以看出:

  1. 刚开始提交任务的时候,线程池中还没有线程,线程池开始创建线程,直到线程数量够用或者达到核心线程数量。然后核心线程开始执行提交的任务(task-1、task-2、task-3、task-4)。
  2. 核心线程刚开始创建了四个,对应执行提交的任务(task-1、task-2、task-3、task-4),后面提交的任务就开始加到任务队列中。可以看到编号5的位置开始执行任务(task-5、task-6),这两个任务就是加入到任务队列中的,直到有空闲线程才开始执行。
  3. 当线程数量达到核心线程数,并且核心线程都没有空闲,任务队列也已经满了的时候,就开始创建新的线程来执行提交的任务。线程数量增加到了最大线程数量。
  4. 当新的任务提交时,此时线程数量达到了最大线程数量,并且没有线程空闲,任务队列也已经满了,就会启用拒绝策略,代码中的为ThreadPoolExecutor.AbortPolicy(),就会直接抛出RejectedExecutionException异常。

查看空闲线程的自动销毁
更改一下代码,让任务数为8,并且主线程睡眠30s,因为之前设定的空闲线程存活时间为20s。30s后查看线程池的线程数量。

		for (int i = 1; i <= 8; i++) {
            pool.submit(new RunTask("task-" + i));
        }
        TimeUnit.SECONDS.sleep(30);
        System.out.println(pool.getPoolSize());

查看运行结果:
线程池任务提交原理,阻塞任务队列与拒绝策略_第6张图片
发现线程数量又回到了核心线程数量。程序运行期间,由于任务队列已满,又新建了线程来处理任务,线程数量=最大线程数量。

当线程数量>核心线程数量的时候,如果线程处于空闲状态的时间超过keepAliveTime,时间单位为unit,该线程就会自动退出销毁。所以最后线程的数量就会收缩到核心线程数量。

线程池任务提交原理总结

  1. 当线程数小于核心线程数时,会创建线程来处理任务。
  2. 当线程数大于等于核心线程数时,如果任务队列没有满,将任务加进任务队列等待线程空闲时调用。
  3. 当线程数大于等于核心线程数,并且任务队列已满时,创建新的线程来执行任务(须保证线程数不大于最大线程数)。
  4. 当有线程调用结束,处于空闲状态,如果任务队列中有任务,则执行任务队列中的任务。
  5. 如果线程数大于核心线程数,并且线程空闲时间超过keepAliveTime,则该线程退出销毁。以保证线程数量收缩为核心线程数。

阻塞队列

  • ArrayBlockingQueue
    由数组组成的有界阻塞队列。
    容量一旦创建,后续无法修改。
    元素是有顺序的,按照先入先出进行排序,从队尾插入数据数据,从队头拿数据;
    队列满时,往队列中 put 数据会被阻塞,队列空时,往队列中拿数据也会被阻塞。

    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    
    /**
     * Creates an {@code ArrayBlockingQueue} with the given (fixed)
     * capacity and the specified access policy.
     *
     * @param capacity the capacity of this queue
     * @param fair if {@code true} then queue accesses for threads blocked
     *        on insertion or removal, are processed in FIFO order;
     *        if {@code false} the access order is unspecified.
     * @throws IllegalArgumentException if {@code capacity < 1}
     */
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
    

    第二个方法有两个参数,第一个参数为阻塞队列容量,第二个参数是否公平主要是读写锁是否公平。如果是公平锁,那么在锁竞争时,就会按照先来先到的顺序,如果是非公平锁,锁竞争时随机的。

    初始容量<=0会抛出IllegalArgumentException异常。

  • LinkedBlockingQueue
    有界链表阻塞队列。

    底层数据结构就是一个链表,链表保存了头节点和尾节点。

    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }
    
    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater
     *         than zero
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
    
    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}, initially containing the elements of the
     * given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }
    

    默认不传参时队列容量时 Integer.MAX_VALUE。
    链表维护先入先出队列,新元素被放在队尾,获取元素从队头部拿;

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
    
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }
    
  • DelayQueue
    延迟队列。本质上是个优先级队列,按照剩余时间的多少排序的队列。

  • SynchronousQueue

    队列不存储数据,所以没有大小,也无法迭代;
    插入操作的返回必须等待另一个线程完成对应数据的删除操作,反之亦然;
    队列由两种数据结构组成,分别是后入先出的堆栈和先入先出的队列,堆栈是非公平的,队列是公平的。

拒绝策略

  • AbortPolicy
    中止策略
    该策略是默认饱和策略。
    抛出一个RejectedExecutionException异常。
	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
  • DiscardPolicy
    抛弃策略,直接丢弃不做任何操作。

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
修改测试用例代码
	ThreadPoolExecutor pool = new ThreadPoolExecutor(4, 6, 20,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadFactory() {

            final private AtomicInteger atomicInteger = new AtomicInteger();

            @Override
            public Thread newThread(Runnable r) {
                //以原子方式将当前值加 1。
                int index = atomicInteger.incrementAndGet();
                System.out.println("create Thread: " + "Thread-" + index);
                return new Thread(r, "Thread-" + index);
            }
        },
//                new ThreadPoolExecutor.AbortPolicy()
                new ThreadPoolExecutor.DiscardPolicy()
        );
	
		for (int i = 1; i <= 10; i++) {
            pool.submit(new RunTask("task-" + i));
        }

运行发现后面提交的任务都被抛弃了。task-9、task-10并没有
线程池任务提交原理,阻塞任务队列与拒绝策略_第7张图片

  • DiscardOldestPolicy
    抛弃旧任务策略,
    将头任务出队。

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }

修改测试用例

//创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(4, 6, 20,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadFactory() {

            final private AtomicInteger atomicInteger = new AtomicInteger();

            @Override
            public Thread newThread(Runnable r) {
                //以原子方式将当前值加 1。
                int index = atomicInteger.incrementAndGet();
                System.out.println("create Thread: " + "Thread-" + index);
                return new Thread(r, "Thread-" + index);
            }
        },
//                new ThreadPoolExecutor.AbortPolicy()
//                new ThreadPoolExecutor.DiscardPolicy()
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );

运行发现task-9和task-10都运行了,但是最早入队的task-5,task-6被抛弃了。
线程池任务提交原理,阻塞任务队列与拒绝策略_第8张图片

  • CallerRunsPolicy
    调用者运行。
    直接调用Runnable的run方法运行。
	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }

修改测试用例

	//创建线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(4, 6, 20,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadFactory() {

            final private AtomicInteger atomicInteger = new AtomicInteger();

            @Override
            public Thread newThread(Runnable r) {
                //以原子方式将当前值加 1。
                int index = atomicInteger.incrementAndGet();
                System.out.println("create Thread: " + "Thread-" + index);
                return new Thread(r, "Thread-" + index);
            }
        },
//                new ThreadPoolExecutor.AbortPolicy()
//                new ThreadPoolExecutor.DiscardPolicy()
//                new ThreadPoolExecutor.DiscardOldestPolicy()
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

运行发现task-9,task-10是main线程执行的。

线程池任务提交原理,阻塞任务队列与拒绝策略_第9张图片

你可能感兴趣的:(Java并发编程,多线程,线程池,并发编程)