高并发之线程池(网课整理)

线程池

线程

  1. 概念
    进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程在执行过程中拥有独立的内存单元,而多个线程共享内存。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间。

  2. 使用线程池的好处
    i. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    ii. 提高响应速度。当任务到达时,不需要等到线程创建就能立即执行。
    iii. 提高线程的可管理性。使用线程池可以进行统一的分配,调优和监控。

  3. 线程的创建与管理
    i. 通过继承Thread类来创建一个线程
    ii. Runnable是个接口,实现该接口并重写run方法,new Thread(runnable).start(),线程启动时就会自动调用该对象的run方法
    iii. Callable是个接口,实现call()方法,使用FutureTask类来包装Callable对象,使用 FutureTask对象作为Thread对象的target创建并启动新线程;也可使用线程池启动

    a. Runnable和Callable的区别是: (1)Callable规定的方法是call(),Runnable规定的方法 是run(). (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得。 (3)call方法可以抛出异常,run方法不可以。 (4)运行Callable任务可以拿到一个Future 对象,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算 的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如果线程没有 执行完,Future.get()方法可能会阻塞当前线程的执行;如果线程出现异常, Future.get()会throws InterruptedException或者ExecutionException;如果线程已 经取消,会跑出CancellationException。取消由cancel 方法来执行。isDone确定任务 是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使 用 Future 但又不提供可用的结果,则可以声明Future<?> 形式类型、并返回 null 作 为底层任务的结果。

    iv. 线程池:Executors 类提供了方便的工厂方法来创建不同类型的 executor services 。无论 是Runnable接口的实现类还是Callable接口的实现类,都可以被ThreadPoolExecutor或 ScheduledThreadPoolExecutor执行

    a. public static ExecutorService newCachedThreadPool() 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。
    b. public static ExecutorService newFixedThreadPool(int nThreads)创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,超出的线程会在队列中等待。
    c. public static ExecutorService newSingleThreadExecutor() 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程,它只会用唯一的工作线程来 执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
    d. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个定长线程池,支持定时及周期性任务执行。
    e. newWorkStealingPool它是新的线程池类ForkJoinPool的扩展,由于能够合理的使用 CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中

    v. ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列 (Deque),里面存放的对象是任务(ForkJoinTask)。

    a. 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的队尾,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从队尾取出任务来执行。
    b. 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务(或是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的工作队列),窃取的任务位于其他线程的工作队列的队首,也就是说工作线程在窃取其他工作线程的任务时,使用的是 FIFO 方 式。
    c. 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
    d. 在既没有自己的任务,也没有可以窃取的任务时,进入休眠。

    vi. ExecutorCompletionService:内部管理者一个已完成任务的阻塞队列,如果阻塞队列中有已完成的任务, take方法就返回任务的结果, 否则阻塞等待任务完成,基于FutureTask实现。

线程池原理

ThreadPoolExecutor

  1. corePoolSize:核心线程数量,当有新任务在execute()方法提交时,会执行以下判断:
    i. 如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
    ii. 如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当 workQueue满时才创建新的线程去处理任务;
    iii. 如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的, 这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
    iv. 如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过 handler所指定的策略来处理任务;
    v. 所以,任务提交时,判断的顺序为 corePoolSize --> workQueue --> maximumPoolSize。
  2. maximumPoolSize:最大线程数量;
  3. workQueue:等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列;
  4. workQueue:保存等待执行的任务的阻塞队列,当提交一个新的任务到线程池以后, 线程池会根据当前线程池中正在运行着的线程的数量来决定对该任务的处理方式,主要有以下几种处理方式:
    i. 直接切换:这种方式常用的队列是SynchronousQueue,但现在还没有研究过该队列,这里暂时还没法介绍;
    ii. 使用无界队列:一般使用基于链表的阻塞队列LinkedBlockingQueue。如果使用这种方 式,那么线程池中能够创建的最大线程数就是corePoolSize,而maximumPoolSize就不会 起作用了(后面也会说到)。当线程池中所有的核心线程都是RUNNING状态时,这时一个 新的任务提交就会放入等待队列中。
    iii. 使用有界队列:一般使用ArrayBlockingQueue。使用该方式可以将线程池的最大线程数量限制为maximumPoolSize,这样能够降低资源的消耗,但同时这种方式也使得线程池对线程的调度变得更困难,因为线程池和队列的容量都是有限的值,所以要想使线程池处理任务的吞吐率达到一个相对合理的范围,又想使线程调度相对简单,并且还要尽可能的降低线程池对资源的消耗,就需要合理的设置这两个数量。
    iv. keepAliveTime:线程池维护线程所允许的空闲时间。当线程池中的线程数量大于 corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
    v. threadFactory:它是ThreadFactory类型的变量,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
    vi. handler:它是RejectedExecutionHandler类型的变量,表示线程池的饱和策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。 线程池提供了4种策略:

    a. AbortPolicy:直接抛出异常,这是默认策略;
    b. CallerRunsPolicy:用调用者所在的线程来执行任务;
    c. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
    d. DiscardPolicy:直接丢弃任务;

核心源码

1. 线程池执行源码

/*
 * ThreadPoolExecutor的execute方法
 */
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // clt 记录着 runState workerCount
    int c = ctl.get();
    /*
	 * workerCountOf 方法取出低29位的值,表示当前活动的线程数 
	 */
    if (workerCountOf(c) < corePoolSize) {
        /*
	     * addWorker 方法的主要工作是在线程池中创建一个新的线程并执行
	     * firstTask 参数,用于指定新增的线程执行的第一个任务
	     * core 参数为
	     * true 表示在新增线程时会判断当前活动线程数是否少于corePoolSize
	     * false 表示在新增线程前需要判断当前活动线程数是否少于maximumPoolSize
	     */
        if (addWorker(command, true))
            return;
        /*
		 * 如果添加失败,则重新获取ctl值 
		 */
        c = ctl.get();
    }
    /* 
     * 如果当前线程池是运行状态并且任务添加到队列成功
	 */
    if (isRunning(c) && workQueue.offer(command)) {
    	// 重新获取ctl值
        int recheck = ctl.get();
        // 再次判断线程池的运行状态,如果不是运行状态,由于之前已经把command添加到workQueue中了
        // 这时需要移除该command
        // 执行过后通过handler使用拒绝策略对该任务进行处理,整个方法返回
        if (! isRunning(recheck) && remove(command))
            reject(command);
        /*
         * 获取线程池中的有效线程数,如果数量是0,则执行addWorker方法
         * 这里传入的参数表示:
         * 1. 第一个参数为null,表示在线程池中创建一个线程,但不去启动;
         * 2. 第二个参数为false,将线程池的有限线程数量的上限设置为maximumPoolSize,添加线程时根据maximumPoolSize来判断;
         * 如果判断workerCount大于0,则直接返回,在workQueue中新增的command会在将来的某个时刻被执行。 
         */
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    /*
     * 如果执行到这里,有两种情况:
     * 1. 线程池已经不是 RUNNING 状态;
     * 2. 线程池是 RUNNING 状态,但 workerCount >= corePoolSize 并且 workQueue 已满。
     * 这时,再次调用addWorker方法,但第二个参数传入为false,将线程池的有限线程数量的上限设置为 maxmumPoolSize;
     * 如果失败则拒绝该任务
     */
    else if (!addWorker(command, false))
        reject(command);
}

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        // 获取运行状态
        int rs = runStateOf(c);

        /*
         * 这个if判断
         * 如果rs >= SHUTDOWN,则表示此时不再接收新任务;
         * 接着判断以下3个条件,只要有1个不满足,则返回 false;
         * 1. rs == SHUTDOWN,这时表示关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
         * 2. firstTask为空
         * 3. 阻塞队列不为空
         *  
         * 首先考虑 rs == SHUTDOWN 的情况
         * 这种情况下不会接受新提交的任务,所以在firstTask不为空的时候会返回false;
         * 然后,如果firstTask为空,并且workQueue也为空,则返回false,
         * 因为队列中已经没有任务了,不需要再添加线程了
         */
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
        	// 获取线程数
            int wc = workerCountOf(c);
            // 如果wc超过CAPACITY,也就是ctl的低29位的最大值(二进制是29个1),返回false;
            // 这里的core是addWorker方法的第二个参数,如果为true表示根据corePoolSize来比较
            // 如果为false则根据maximumPoolSize来比较
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            // 尝试增加workerCount,如果成功,则跳出第一个for循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // 如果增加workerCount失败,则重新获取ctl的值
            c = ctl.get();  // Re-read ctl
            // 如果当前的运行状态不等于rs,说明状态已被改变,返回第一个for循环继续执行
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
    	// 根据firstTask来创建worker对象
        w = new Worker(firstTask);
        // 每一个worker对象都会创建一个线程
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());
				// rs < SHUTDOWN 表示是 RUNNING 状态
				// 如果rs是RUNNING状态或者rs是SHUTDOWN状态并且firstTask为null,向线程池中添加线程。
				// 因为在SHUTDOWN时不会再添加新的任务,但还是会执行workQueue中的任务
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    // workers是一个hashset
                    workers.add(w);
                    int s = workers.size();
                    // largestPoolSize 记录着线程池中出现过的最大线程数量
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
            	// 启动线程
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

2. 重点

  1. 线程复用的原理与线程如何回收

    当allowCoreThreadTimeOut=true或者此时工作线程大于corePoolSize时,线程调用 BlockingQueue的poll方法获取任务,若超过keepAliveTime时间,则返回null, timedOut=true,则getTask会返回null,线程中的runWorker方法会退出while循环,线程接下来会被回收。

  2. 如果allowCoreThreadTimeOut=true,核心线程在规定时间内会被回收。
// ThreadPoolExecutor的getTask方法
private Runnable getTask() {
	// timeout 变量的值表示上次从阻塞队列中取任务时是否超时
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        /*
         * 如果线程池状态 rs >= SHUTDOWN,也就是非RUNNING状态,再进行以下判断:
         * 1. rs >= STOP,线程池是否正在stop;
         * 2. 阻塞队列是否为空。
         * 如果以上条件满足,则将workerCount减1并返回null
         * 因为如果当前线程池状态的值是SHUTDOWN或以上时,不允许再向阻塞队列中添加任务
         */
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        // timed 变量用于判断是否需要进行超时控制
        // allowCoreThreadTimeOut 默认是false,也就是核心线程不允许进行超时
        // wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量
        // 对于超过核心线程数量的这些线程,需要进行超时控制
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
		
		/*
		 * wc > maxmumPoolSize 的情况是因为可能在此方法执行阶段同时执行了setMaximumPoolSize方法;
		 * timed & timedOut 如果为 true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时
		 * 接下来判断,如果有效线程数量大于1,或者阻塞队列是空的,那么尝试将workerCount减一;
		 * 如果减一失败,则返回重试。
		 * 如果 wc == 1时,也就说明当前线程是线程池中唯一的一个线程了。
		 */
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
        	/*
        	 * 保证核心线程不被销毁
        	 * 根据timed来判断,如果为true,则通过阻塞队列的poll方法进行超时控制,如果在keepAliveTime 时间内没有获取到任务,则返回null;
        	 * 否则通过take方法,如果这时队列为空,则take方法会阻塞直到队列不为空。
        	 */
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            // 如果 r == null,说明已经超时,timeOut设置为true
            timedOut = true;
        } catch (InterruptedException retry) {
        	// 如果获取任务时当前线程发生了中断,则设置timeOut为false并返回循环重试
            timedOut = false;
        }
    }
}

FutureTask源码

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    // 计算等待截止时间
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
    	// 1. 判断阻塞线程是否被中断,如果被中断则在等待队列中删除该节点并抛出InterruptedException异常
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
		// 2. 获取当前状态,如果状态大于COMPLETING
		// 说明任务已经结束(要么正常结束,要么异常结束,要么被取消)
		// 则把thread显示置空,并返回结果
        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        // 3. 如果状态处于中间状态COMPLETING
        // 表示任务已经结束但是任务执行线程还没来得及给outcome赋值
        // 这个时候让出执行权让其他线程优先执行
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        // 4. 如果等待节点为空,则构造一个等待节点
        else if (q == null)
            q = new WaitNode();
        // 5. 如果还没有入队列,则把当前节点加入waiters首节点并替换原来waiters
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        else if (timed) {
        	// 如果需要等待特定时间,则先计算要等待的时间
        	// 如果已经超时,则删除对应节点并返回对应的状态
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            // 6. 阻塞等待特定时间
            LockSupport.parkNanos(this, nanos);
        }
        else
        	// 6. 阻塞等地直到被其他线程唤醒
            LockSupport.park(this);
    }
}

不管是任务执行异常还是任务正常执行完毕,或者取消任务,最后都会调用finishCompletion()方法。

private void finishCompletion() {
    // assert state > COMPLETING;
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }

    done();

    callable = null;        // to reduce footprint
}

线程池中的线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

  • prestartCoreThread():初始化一个核心线程;
  • prestartAllCoreThreads():初始化所有核心线程

线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止, 但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

异常处理

  • execute:Apache Commons 提供的BasicThreadFactoryBuilder
  • submit:get try+catch

任务拒绝策略

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(默认)。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务 (重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

线程池大小

  1. 粗略
    i. 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
    ii. 如果是IO密集型任务,参考值可以设置为2*NCPU
  2. 精确:((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
  3. 最佳:压测

任务缓存队列

在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。 BlockingQueue 是个接口,你需要使用它的实现之一来使用BlockingQueue,java.util.concurrent 包下具有以下 BlockingQueue 接口的实现类:

  • ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放 到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改 了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
  • LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行 存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
  • DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
  • PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排 序就取决于你自己的 Comparable 实现。
  • SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个 元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。

线程池总结

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    i. 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    ii. 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    iii. 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    iv. 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
    v. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
    vi. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后, 它最终会收缩到 corePoolSize 的大小。

Executor框架

Java线程既是工作单元,也是执行机制。从JDK 5开始,把工作单元与执行机制分离开来。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

Executor框架简介

Executor框架两级调度模型

  • 上层,Java多线程程序将应用分解为若干个任务,然后使用用户级调度器(Executor框架)将这 些任务映射为固定数量的线程
  • 底层,操作系统内核将这些线程映射到硬件处理器上

可以这么理解,应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核控制,下层的调度不受应用程序的控制。

Executor框架结构和成员

Executor框架主要由3大部分组成:

  • 任务。包括被执行任务需要实现的接口:Runnable接口或Callable接口
  • 任务的执行。包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService 接口。两个关键类实现了ExecutorService(ThreadPoolExecutor和 ScheduledThreadPoolExecutor)
  • 异步计算结果。包括接口Future和实现Future接口的FutureTask类。

Executor框架的主要成员:

  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor
  • Future接口
  • Runnable接口、Callable接口
  • Executors

ThreadPoolExecutor

ThreadPoolExecutor通常使用工厂类Executors来创建,Executors可以创建3种类型的 ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool、CachedThreadPool。

下面分别介绍这3种ThreadPoolExecutor。

1)FixedThreadPool。下面是Executors提供的,创建使用固定线程数的FixedThreadPool的API

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

FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

通过ThreadPoolExecutor的构造方法可以看出,工厂类传递的第一个参数和第二个参数都设置成了 nThreads。即线程池的初始值和最大值都设置为了指定的线程数量。 keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程仍然没有可以执行的任务将会被终止,这里设置成0L表示多余的空闲线程会被立即终止。

通过构造参数可以看出,线程池的构造参数中还传入了一个阻塞队列,该阻塞队列的作用其实就是作为一个任务的缓冲区。

现在可以分析一下该线程池的工作流程如下:

  1. 如果当前运行的线程数量少于corePoolSize,则创建新线程来执行任务。
  2. 在线程池中当前运行的线程数量等于corePoolSize,由于无法再创建新的线程进行任务处理,所 以会将任务加入到阻塞队列LinkedBlockingQueue中进行排队等候处理。
  3. 当线程池中的线程执行完一个任务后,就会去阻塞队列LinkedBlockingQueue中循环获取新的任务继续执行。

所以外部线程可以一直提交任务到线程池中阻塞队列中,然后,线程池中的工作线程在获取任务进行执行,这里其实是一个典型的生产者-消费者模式。

但是外部线程提交的任务数量有限制么,这就要看传入的阻塞队列是否有容量限制,从源码可以看出 是传入的一个没有指定构造参数的LinkedBlockingQueue,发现这里没有指定容量的大小,可以该队列的构造方法实现中去查看源码如下:

/**
 * Creates a {@code LinkedBlockingQueue} with a capacity of
 * {@link Integer#MAX_VALUE}.
 */
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

发现没有传入任务参数的时候,队列会默认创建容量大小为Integer.MAX_VALUE的阻塞队列。

2)SingleThreadExecutor。下面是Executors提供的,创建使用单个线程的 SingleThreadExecutor的API

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

SingleThreadExecutor适用于需要保证顺序执行各个任务;并且在任意时间点,不会有多个线程是活动着的应用场景。

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与 FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的 工作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool相同。

执行流程如下:

  1. 如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线程来 执行任务。
  2. 在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入 LinkedBlockingQueue。
  3. 线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来执行。

3)CachedThreadPool。下面是Executors提供的,创建一个会根据需要而创建新线程的 CachedThreadPool的API

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

CachedThreadPool是大小无界的线程池,适用于执行很多短期异步任务的小程序,或者是负载比较轻的服务器。

CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为 Integer.MAX_VALUE,即maximumPool是无界的。这里把keepAliveTime设置为60L,意味着 CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。

FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作 队列。 CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但 CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下, CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

执行流程如下:

  1. 首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线 程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行, execute()方法执行完成;否则执行下面的步骤2)。
  2. 当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步 骤1)失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
  3. 在步骤2)中新创建的线程将任务执行完后,会执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会 让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务 (主线程执行步骤1)),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

SynchronousQueue是一个没有容量的阻塞队列。 每个插入操作必须等待另一个线程的对应移除操 作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线 程执行。

SynchronousQueue是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。

ScheduledThreadPoolExecutor

带着BAT大厂的面试问题理解ScheduledThreadPoolExecutor:

  • ScheduledThreadPoolExecutor要解决什么样的问题?
  • ScheduledThreadPoolExecutor相比 ThreadPoolExecutor有哪些特性?
  • ScheduledThreadPoolExecutor有什么样的数据结构,核心内部类和抽象类?
  • ScheduledThreadPoolExecutor有哪两个关闭策略? 区别是什么?
  • ScheduledThreadPoolExecutor中scheduleAtFixedRate 和 scheduleWithFixedDelay区别是什么?
  • 为什么ThreadPoolExecutor 的调整策略却不适用于 ScheduledThreadPoolExecutor?
  • Executors 提供了几种方法来构造 ScheduledThreadPoolExecutor?

ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor,为任务提供延迟或周期执行,属于线程池的一种。和 ThreadPoolExecutor 相比,它还具有以下几种特性:

  • 使用专门的任务类型—ScheduledFutureTask 来执行周期任务,也可以接收不需要时间调度的任务(这些任务通过 ExecutorService 来执行)。
  • 使用专门的存储队列—DelayedWorkQueue 来存储任务,DelayedWorkQueue 是无界延迟队列DelayQueue 的一种。相比ThreadPoolExecutor也简化了执行机制(delayedExecute方法, 后面单独分析)。
  • 支持可选的run-after-shutdown参数,在池被关闭(shutdown)之后支持可选的逻辑来决定是否继续运行周期或延迟任务。并且当任务(重新)提交操作与 shutdown 操作重叠时,复查逻辑也不相同。

ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor,ScheduledThreadPoolExecutor 内部构造了两个内部类 ScheduledFutureTask 和 DelayedWorkQueue:

  • ScheduledFutureTask: 继承了FutureTask,说明是一个异步运算任务;最上层分别实现了 Runnable、Future、Delayed接口,说明它是一个可以延迟执行的异步运算任务。
  • DelayedWorkQueue: 这是 ScheduledThreadPoolExecutor 为存储周期或延迟任务专门定义的一个延迟队列,继承了 AbstractQueue,为了契合 ThreadPoolExecutor 也实现了 BlockingQueue 接口。它内部只允许存储 RunnableScheduledFuture 类型的任务。与 DelayQueue 的不同之处就是它只允许存放 RunnableScheduledFuture 对象,并且自己实现了二叉堆(DelayQueue 是利用了 PriorityQueue 的二叉堆结构)。

内部类ScheduledFutureTask

private class ScheduledFutureTask<V>
              extends FutureTask<V> implements RunnableScheduledFuture<V> {
	// 为相同延时任务提供的顺序编号
    private final long sequenceNumber;
	// 任务可以执行的时间,纳秒级
    private long time;
	// 重复任务的执行周期时间,纳秒级
    private final long period;
	// 重新入队的任务
    RunnableScheduledFuture<V> outerTask = this;
	// 延迟队列的索引,以支持更快的取消操作
    int heapIndex;
}
  • sequenceNumber: 当两个任务有相同的延迟时间时,按照 FIFO 的顺序入队。
  • sequenceNumber 就是为相同延时任务提供的顺序编号。
  • time: 任务可以执行时的时间,纳秒级,通过triggerTime方法计算得出。
  • period: 任务的执行周期时间,纳秒级。正数表示固定速率执行(为scheduleAtFixedRate提供服 务),负数表示固定延迟执行(为scheduleWithFixedDelay提供服务),0表示不重复任务。
  • outerTask:重新入队的任务,通过reExecutePeriodic方法入队重新排序

核心方法run()

public void run() {
    boolean periodic = isPeriodic(); // 是否为周期任务
    if (!canRunInCurrentRunState(periodic)) // 当前状态是否可以执行
        cancel(false);
    else if (!periodic)
    	// 不是周期任务,可以执行
        ScheduledFutureTask.super.run();
    else if (ScheduledFutureTask.super.runAndReset()) {
        setNextRunTime(); // 设置下一次运行时间
        reExecutePeriodic(outerTask); // 重排序下一个周期任务
    }
}

ScheduledFutureTask 的run方法重写了 FutureTask 的版本,以便执行周期任务时重置/重排序任务。任务的执行通过父类 FutureTask 的run实现。内部有两个针对周期任务的方法:

  • setNextRunTime():用来设置下一次运行的时间,源码如下:
// 设置下一次执行任务的时间
private void setNextRunTime() {
    long p = period;
    if (p > 0) // 固定速率执行,scheduleAtFixedRate
        time += p;
    else
        time = triggerTime(-p); // 固定延迟执行,scheduleWithFixedDelay
}

// 计算固定延迟任务的执行时间
private long triggerTime(long delay, TimeUnit unit) {
    return triggerTime(unit.toNanos((delay < 0) ? 0 : delay));
}
  • reExecutePeriodic():周期任务重新入队等待下一次执行,源码如下:
// 重排序一个周期任务
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
    if (canRunInCurrentRunState(true)) { // 池关闭后可继续执行
        super.getQueue().add(task); // 任务入列
        // 重新检查run‐after‐shutdown参数,如果不能继续运行就移除队列任务,并取消任务的执行
        if (!canRunInCurrentRunState(true) && remove(task))
            task.cancel(false);
        else
            ensurePrestart(); // 启动一个新的线程等待任务
    }
}

reExecutePeriodic与delayedExecute的执行策略一致,只不过reExecutePeriodic不会执行拒绝策略而是直接丢掉任务。

cancel方法

public boolean cancel(boolean mayInterruptIfRunning) {
      boolean cancelled = super.cancel(mayInterruptIfRunning);
      if (cancelled && removeOnCancel && heapIndex >= 0)
          remove(this);
      return cancelled;
}

ScheduledFutureTask.cancel本质上由其父类 FutureTask.cancel 实现。取消任务成功后会根据 removeOnCancel参数决定是否从队列中移除此任务。

核心属性

// 关闭后继续执行已经存在的周期任务
private volatile boolean continueExistingPeriodicTasksAfterShutdown;
// 关闭后继续执行已经存在的延时任务
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
// 取消任务后移除
private volatile boolean removeOnCancel = false;
// 为相同延时的任务提供的顺序编号,保证任务之间的FIFO顺序
private static final AtomicLong sequencer = new AtomicLong();
  • continueExistingPeriodicTasksAfterShutdown和 executeExistingDelayedTasksAfterShutdown是 ScheduledThreadPoolExecutor 定义的 run-after-shutdown 参数,用来控制池关闭之后的任务执行逻辑。
  • removeOnCancel用来控制任务取消后是否从队列中移除。当一个已经提交的周期或延迟任务在 运行之前被取消,那么它之后将不会运行。默认配置下,这种已经取消的任务在届期之前不会被 移除。 通过这种机制,可以方便检查和监控线程池状态,但也可能导致已经取消的任务无限滞 留。为了避免这种情况的发生,我们可以通过setRemoveOnCancelPolicy方法设置移除策略, 把参数removeOnCancel设为true可以在任务取消后立即从队列中移除。
  • sequencer是为相同延时的任务提供的顺序编号,保证任务之间的 FIFO 顺序。与 ScheduledFutureTask 内部的sequenceNumber参数作用一致。

构造函数

看下构造函数,ScheduledThreadPoolExecutor 内部有四个构造函数,这里我们只看这个最大构造灵活度的:

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

构造函数都是通过super调用了ThreadPoolExecutor的构造,并且使用特定等待队列 DelayedWorkQueue

核心方法:Schedule

public ScheduledFuture<?> schedule(Runnable command,
                                   long delay,
                                   TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    RunnableScheduledFuture<?> t = decorateTask(command,
        new ScheduledFutureTask<Void>(command, null,
                                      triggerTime(delay, unit))); // 构造ScheduledFutureTask任务
    delayedExecute(t); // 任务执行主方法
    return t;
}

schedule主要用于执行一次性(延迟)任务。函数执行逻辑分两步:

  • 封装 Callable/Runnable: 首先通过triggerTime计算任务的延迟执行时间,然后通过 ScheduledFutureTask 的构造函数把 Runnable/Callable 任务构造为 ScheduledThreadPoolExecutor可以执行的任务类型,最后调用decorateTask方法执行用户自定义的逻辑;decorateTask是一个用户可自定义扩展的方法,默认实现下直接返回封装的 RunnableScheduledFuture任务,源码如下:
protected <V> RunnableScheduledFuture<V> decorateTask(
   Runnable runnable, RunnableScheduledFuture<V> task) {
   return task;
}
  • 执行任务:通过delayedExecute实现。下面我们来详细分析。
private void delayedExecute(RunnableScheduledFuture<?> task) {
     if (isShutdown())
         reject(task); // 池已关闭,执行拒绝策略 
     else {
         super.getQueue().add(task); // 任务入队
         if (isShutdown() &&
             !canRunInCurrentRunState(task.isPeriodic()) && // 判断run‐after‐shutdown参数
             remove(task)) // 移除任务
             task.cancel(false);
         else
             ensurePrestart(); // 启动一个新的线程等待任务
	} 
}

delayedExecute是执行任务的主方法,方法执行逻辑如下:

  • 如果池已关闭(ctl >= SHUTDOWN),执行任务拒绝策略
  • 池正在运行,首先把任务入队排序;然后重新检查池的关闭状态,执行如下逻辑:

A: 如果池正在运行,或者 run-after-shutdown 参数值为true,则调用父类方法ensurePrestart启动一个新的线程等待执行任务。ensurePrestart源码如下:

void ensurePrestart() {
    int wc = workerCountOf(ctl.get());
    if (wc < corePoolSize)
    	addWorker(null, true);
	else if (wc == 0)
    	addWorker(null, false);
}

ensurePrestart是父类 ThreadPoolExecutor 的方法,用于启动一个新的工作线程等待执行任务,即使corePoolSize为0也会安排一个新线程。

B: 如果池已经关闭,并且 run-after-shutdown 参数值为false,则执行父类(ThreadPoolExecutor) 方法remove移除队列中的指定任务,成功移除后调用ScheduledFutureTask.cancel取消任务

核心方法:scheduleAtFixedRate 和 scheduleWithFixedDelay

/**
 * 创建一个周期执行的任务,第一次执行延期时间为initialDelay
 * 之后每隔period执行一次,不等待第一次执行完成就开始即时
 */
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0)
        throw new IllegalArgumentException();
    // 构建RunnableScheduledFuture任务类型
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit), // 计算任务的延迟时间
                                      unit.toNanos(period)); // 计算任务的执行周期
    RunnableScheduledFuture<Void> t = decorateTask(command, sft); // 执行用户自定义逻辑
    sft.outerTask = t; // 赋值给outerTask,准备重新入队等待下一次执行
    delayedExecute(t); // 执行任务
    return t;
}

/**
 * 创建一个周期执行的任务,第一次执行延期任务为initialDelay
 * 在第一次执行完之后延迟delay后开始下一次执行
 */
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                             	 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (delay <= 0)
        throw new IllegalArgumentException();
    // 构建RunnableScheduledFuture任务类型
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),// 计算任务的延迟时间
                                      unit.toNanos(-delay));// 计算任务的执行周期
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);// 执行用户自定义逻辑
    sft.outerTask = t;// 赋值给outerTask,准备重新入队等待下一次执行
    delayedExecute(t);// 执行任务
    return t;
}

scheduleAtFixedRate和scheduleWithFixedDelay方法的逻辑与schedule类似。

注意scheduleAtFixedRate和scheduleWithFixedDelay的区别: 乍一看两个方法一模一样,其 实,在unit.toNanos这一行代码中还是有区别的。没错,scheduleAtFixedRate传的是正值,而 scheduleWithFixedDelay传的则是负值,这个值就是 ScheduledFutureTask 的period属性

核心方法:shutdown()

public void shutdown() {
    super.shutdown();
}

@Override void onShutdown() {
    BlockingQueue<Runnable> q = super.getQueue();
    // 获取run‐after‐shutdown参数
    boolean keepDelayed =
        getExecuteExistingDelayedTasksAfterShutdownPolicy();
    boolean keepPeriodic =
        getContinueExistingPeriodicTasksAfterShutdownPolicy();
    if (!keepDelayed && !keepPeriodic) { // 池关闭后不保留任务
    	// 依次取消任务
        for (Object e : q.toArray())
            if (e instanceof RunnableScheduledFuture<?>)
                ((RunnableScheduledFuture<?>) e).cancel(false);
        q.clear(); // 消除等待队列
    }
    else { // 池关闭后保留任务
        // Traverse snapshot to avoid iterator exceptions
        // 遍历快照以避免迭代器异常
        for (Object e : q.toArray()) {
            if (e instanceof RunnableScheduledFuture) {
                RunnableScheduledFuture<?> t =
                    (RunnableScheduledFuture<?>)e;
                if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) ||
                    t.isCancelled()) { // also remove if already cancelled
                    // 如果任务已经取消,移除队列中的任务
                    if (q.remove(t))
                        t.cancel(false);
                }
            }
        }
    }
    tryTerminate(); // 终止线程池
}

说明: 池关闭方法调用了父类ThreadPoolExecutor的shutdown,主要介绍以下在shutdown方法中 调用的关闭钩子onShutdown方法,它的主要作用是在关闭线程池后取消并清除由于关闭策略不应该 运行的所有任务,这里主要是根据 run-after-shutdown 参数 (continueExistingPeriodicTasksAfterShutdown和executeExistingDelayedTasksAfterShutdown) 来决定线程池关闭后是否关闭已经存在的任务

再深入理解

  • 为什么ThreadPoolExecutor 的调整策略却不适用于 ScheduledThreadPoolExecutor?
    例如: 由于 ScheduledThreadPoolExecutor 是一个固定核心线程数大小的线程池,并且使用了一个 无界队列,所以调整maximumPoolSize对其没有任何影响(所以 ScheduledThreadPoolExecutor 没 有提供可以调整最大线程数的构造函数,默认最大线程数固定为Integer.MAX_VALUE)。此外,设置 corePoolSize为0或者设置核心线程空闲后清除(allowCoreThreadTimeOut)同样也不是一个好的策 略,因为一旦周期任务到达某一次运行周期时,可能导致线程池内没有线程去处理这些任务。
  • Executors 提供了哪几种方法来构造 ScheduledThreadPoolExecutor?
    ScheduledThreadPoolExecutor通常使用工厂类Executors来创建,Executors可以创建2种类型的 ScheduledThreadPoolExecutor,如下:
  • ScheduledThreadPoolExecutor。包含若干个线程的ScheduledThreadPoolExecutor。
  • SingleThreadScheduledExecutor。只包含一个线程的ScheduledThreadPoolExecutor。

1)ScheduledThreadPoolExecutor。下面是Executors提供的,创建固定个数的线程 ScheduledThreadPoolExecutor的API。

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

ScheduledThreadPoolExecutor适用于需要多个后台线程执行的周期任务,同时为了满足资源管理的需求而需要限制后台线程数量的场景。

2)SingleThreadSchduledExecutor。下面是Executors提供的,创建单个线程的 SingleThreadSchduledExecutor的API。

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

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

SingleThreadSchduledExecutor适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的场景。

Future接口

Future接口和实现Future接口的FutureTask类用来表示异步计算的结果,当我们把Runnable接口或 者Callable接口的实现类提交(submit)给ThreadPoolExecutor或者 ScheduledThreadPoolExecutor时,ThreadPoolExecutor或者ScheduledThreadPoolExecutor会 向我们返回一个FutureTask对象。下面是对应的API

public Future<?> submit(Runnable task) {
            return e.submit(task);
        }
public <T> Future<T> submit(Callable<T> task) {
            return e.submit(task);
        }
public <T> Future<T> submit(Runnable task, T result) {
            return e.submit(task, result);
}

Runnable和Callable接口

Runnable和Callable接口的实现类都可以被ThreadPoolExecutor或者 SchduledThreadPoolExecutor执行。它们之间的区别是Runnable不会返回结果,而Callable可以返回结果。

除了可以自已创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个Runnable包装成一个Callable。

下面是Executors提供的,把一个Runnable包装成Callable的API

public static Callable<Object> callable(Runnable task) {
       if (task == null)
           throw new NullPointerException();
       return new RunnableAdapter<Object>(task, null);
}

下面是Executors提供的,把一个Runnable和一个待返回的结果包装成一个Callable的API

public static <T> Callable<T> callable(Runnable task, T result) {
          if (task == null)
               throw new NullPointerException();
          return new RunnableAdapter<T>(task, result);
}

当我们把一个Callable对象提交给ThreadPoolExecutor或者SchduledThreadPoolExecutor执行 时,summit()会向我们返回一个FutureTask对象。我们可以执行FutureTask.get()来等待任务执行完成。当任务完成后FutureTask.get()将会返回任务的结果。

Spring中的线程安全性

Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。

Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如, 一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。

  • singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象 的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。
  • prototype:bean被定义为在每次注入时都会创建一个新的对象。
  • request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。
  • session:bean被定义为在一个session的生命周期内创建一个单例对象。
  • application:bean被定义为在ServletContext的生命周期中复用一个单例对象。
  • websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

我们交由Spring管理的大多数对象其实都是一些无状态的对象,这种不会因为多线程而导致状态被破 坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的(也可以说只要是无状态的对象,不管单例多例都是线程安全的,不过单例毕竟节省了不断创建对象与GC的开销)。

无状态的对象即是自身没有状态的对象,自然也就不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Service、DAO和Controller,这些对象并没有自己的状态,它们只是用来执行某些操作 的。例如,每个DAO提供的函数都只是对数据库的CRUD,而且每个数据库Connection都作为函数 的局部变量(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程 安全问题),用完即关(或交还给连接池)。

有人可能会认为,我使用request作用域不就可以避免每个请求之间的安全问题了吗?这是完全错误 的,因为Controller默认是单例的,一个HTTP请求是会被多个线程执行的,这就又回到了线程的安全问题。当然,你也可以把Controller的scope改成prototype,实际上Struts2就是这么做的,但有一 点要注意,Spring MVC对请求的拦截粒度是基于每个方法的,而Struts2是基于每个类的,所以把 Controller设为多例将会频繁的创建与回收对象,严重影响到了性能。

通过上面讲述其实已经说的很清楚了,Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等 这些实现线程同步的方法了。

猜你喜欢

转载自blog.csdn.net/qq_24095055/article/details/105851404