线程池原理分析

183 阅读9分钟
1.线程池的优势(为什么要使用线程池)
  1. 避免线程池的创建和销毁的浪费
  2. 更快的响应提交的任务 (实现线程的复用)
  3. 便于管理线程
2.创建线程池的方式

创建线程池有两种方式:通过Executors创建和ThreadPoolExecutor创建,应该尽量避免使用Executors创建线程池,因为可能有资源耗尽的风险

1.FixedThreadPool和SingleThreadPool中的阻塞队列大小为Integer.MAX_VALUE,可能会堆积大量的请求从而导致OOM
2.CacheThreadPool的最大线程数量为Integer.MAX_VALUE,可能会创建大量的线程从而导致OOM
3.线程池的工作原理

创建线程有几个核心的参数:核心线程数、最大线程数、线程存活时间、任务阻塞队列和拒绝策略

public void execute(Runnable command) {
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

当任务提交到线程池的时候:

  1. 若当前的工作线程数量小于核心工线程数量,则创建一个新的核心线程并执行任务
  2. 若当前工作线程的数量等于核心线程数,则将任务提交到阻塞队列中。这个时候进行一次二次检查,若线程池已经被shutDown,则从阻塞队列中移除该任务;否则检查当前线程池中的线程数是否有线程,如果没有线程,则添加一个非核心线程
  3. 若阻塞队列已经满,则创建一个非核心线程执行该任务
  4. 若线程池中的工作线程数量已经到达了线程池的最大数量,则执行拒绝策略
4.线程池中的核心线程和非核心线程如何进行设置
  1. CPU密集型:CPU+1
  2. IO密集型:CPU*2
5.线程池中线程数量以及运行状态是如何获取

在ThreadPoolExecutors中维护了一个AutomicInteger对象ctl,该对象有32位,高3位用于计算当前线程池的状态,低29位用于计算当前线程池中运行的线程数

6.根据添加线程执行任务的逻辑代码,若发生以下场景线程池是如何应对的?
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            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 {
        w = new Worker(firstTask);
        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());

                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}
  • addWorker()添加线程失败的时候,线程池是如何处理?
for (;;) {
    int wc = workerCountOf(c);
    if (wc >= CAPACITY ||
        wc >= (core ? corePoolSize : maximumPoolSize))
        return false;
    if (compareAndIncrementWorkerCount(c))
        break retry;
    c = ctl.get();  // Re-read ctl
    if (runStateOf(c) != rs)
        continue retry;
    // else CAS failed due to workerCount change; retry inner loop
}

在addWorker内部有一个for循环,使用CAS操作将当前ctl内存的值原子加一。如果成功则通过break retry退出循环,接着创建Worker对象并在内部通过ThreadFactory创建线程;若失败则继续执行for循环,反复通过CAS操作将ctl内存值原子加一,若获取当前创建的线程数大于最大线程数或者核心线程数则返回false.

若添加Worker对象成功,则调用其中的线程的start方法,使线程开始处于运行状态

7.线程池中的线程是如何执行工作的,如何在执行完任务不被销毁,如何保持线程存活时间?

线程池中的线程启动后执行到Worker中的run()方法,Worker属于ThreadPoolExecutor的内部类,持有外部类的引用,因此可以调用其runWorker()方法

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

在runWorker()方法中,执行Worker中的Runnable任务很简单,只需要执行Runnable中的run()方法,那么线程如何在执行完任务不会被销毁呢。这就归功于阻塞队列,线程执行完任务后,从线程池中不断的取出阻塞队列中的任务执行,阻塞队列,当然是阻塞的,阻塞队列基于生产者-消费者模型,如果队列中的数量不为0就通知线程可以消费任务,若队列中的任务数量为0就阻塞超时时间,具体的执行逻辑见getTask()方法

private Runnable getTask() {
    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.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

getTask()也是一个for循环中执行业务逻辑:

  1. 如果线程池调用了shutDown(状态是SHUTDOWN)或者调用shutDownNow(属于STOP状态)或者队列中不存在任务,则返回空。getTask()中的while循环退出,核心线程+非核心线程被销毁
  2. 如果线程工作数量大于最大线程数,或者属于允许超时的线程并且已经超时一次并且线程池中的工作线程数大于1,则返回空。getTask()中的while循环退出,非核心线程被销毁
  3. 否则从阻塞队列中获取任务,获取任务属于阻塞式获取,核心线程一直阻塞,非核心线程阻塞keepAliveTime,这也是线程执行完任务后线程没有被销毁的原因
8.线程池调用shutdown()之后如何等待正在执行的任务执行完,然后进行任务销毁的?
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

清理线程的主要工作还是在interruptIdleWorkers中

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

遍历当前线程池中所有的Worker,如果Worker绑定的线程还未被中断,则首先尝试获取Worker中对应的锁,这一步即对应等待正在执行的任务结束再执行清理工作,原因是若线程正在执行任务,则已经获取到了Worker中对应的锁,tryLock是处于阻塞的状态,当任务执行完毕,此处tryLock开始执行,接下来通过interrupt中断线程,然后进行销毁

线程销毁的原理是: 对于刚刚执行完任务的线程,从队列中获取任务时候发现线程池已经处于SHUTDOWN或者STOP状态则直接返回空,runWorker的while循环退出,线程自动结束 对于正在keepLiveTime的线程

  1. 依据Thread的interrupt()方法,该方法针对正在运行中的线程没有作用,只能添加一个中断的标志位,针对阻塞的线程,则会抛出InterruptedException
  2. 接着在getTask()方法中捕获到异常,退出阻塞,重新执行到while循环中,发现线程池的已经处于SHUTDOWN状态,则返回null,runWorker中的循环也退出,线程自动结束
9.线程池调用shutdownNow()如何立即销毁线程
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

主要的实现方法在interruptWorkers()方法中

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}

接着又执行了Wokrer的interruptIfStarted()方法

void interruptIfStarted() {
    Thread t;
    if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
        try {
            t.interrupt();
        } catch (SecurityException ignore) {
        }
    }
}

很明显,这个地方没有等待任务执行结束直接调用了interrupt()方法,但是该方法只能对线程设置中断标志位,并不能立即销毁线程,因此shutdownNow也有可能需要等待所有的任务执行结束之后再去销毁

10.线程的中断机制

线程的中断机制是通过Thread的interrupt()方法,该方法只能对线程添加中断标志位,如果想要中断立即生效,则需要调用Thread.interrupted()方法来触发。而对于阻塞的线程sleep,wait,join,调用Interrupt方法会立即退出阻塞状态,并且抛出InterruptedException异常,因此对于线程阻塞函数,需要正确处理异常

11.线程池的阻塞队列有哪些
  1. ArrayBlockingWueue:自定义大小的阻塞队列
  2. LinkedBlockingQueue:可设置容量的队列,先进先出
  3. PriorityBlockingQueue:优先级队列,无界队列
12.线程池的拒绝策略
  1. CallerRunPolicy:当前线程直接执行
  2. AbortPolicy:拒绝并抛出异常
  3. DiscardPolicy:拒绝但是不抛出异常
  4. DiscardOldestPolicy:将阻塞队列中最后的任务移除,将任务提交到队列中
13.线程池的几种状态
  1. RUNNING:线程池正在运行并处理任务
  2. SHUTDOWN:(shuwdown方法)线程池不接收新任务,但是处理阻塞队列中的任务
  3. STOP:(shutdownNow)线程池不接收新任务,且不处理阻塞队列中的任务
  4. TYDING:当处于SHUTDOWN或者STOP状态下,并且任务队列中没有任务的时候切换到此状态
  5. TERMINATED:当terminated()方法执行完毕,将彻底进入此状态
14.运行状态的线程池重新设置线程超时时间会如何
  • 对于正在运行中的线程,自然影响不大,因为线程还没有进入到超时阻塞状态
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

if ((wc > maximumPoolSize || (timed && timedOut))
    && (wc > 1 || workQueue.isEmpty())) {
    if (compareAndDecrementWorkerCount(c))
        return null;
    continue;
}

try {
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    if (r != null)
        return r;
    timedOut = true;
} catch (InterruptedException retry) {
    timedOut = false;
}
  • 对于超时阻塞的线程,调用interruptIdleWorkers()方法中断空闲线程,则捕获到异常后再一次进入循环,核心线程和非核心线程均等待超时时间,超时时间到了后销毁非核心线程,若此时的线程数等于核心线程数,timedOut为true,timed为false,则执行take进行阻塞,实现了核心线程的永不销毁
15.运行中的线程池修改最大线程数量会如何
public void setMaximumPoolSize(int maximumPoolSize) {
    if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
        throw new IllegalArgumentException();
    this.maximumPoolSize = maximumPoolSize;
    if (workerCountOf(ctl.get()) > maximumPoolSize)
        interruptIdleWorkers();
}

对于设置的maximumPoolSize更大,则无什么影响;若更小,则需要清理掉一些非核心线程,同样调用的是interruptIdleWorkers()方法,对于设置的maximumPoolSize小于当前线程池的核心线程数量则会抛出异常

16.运行中的线程池修改核心线程数量会如何
public void setCorePoolSize(int corePoolSize) {
    if (corePoolSize < 0)
        throw new IllegalArgumentException();
    int delta = corePoolSize - this.corePoolSize;
    this.corePoolSize = corePoolSize;
    if (workerCountOf(ctl.get()) > corePoolSize)
        interruptIdleWorkers();
    else if (delta > 0) {
        // We don't really know how many new threads are "needed".
        // As a heuristic, prestart enough new workers (up to new
        // core size) to handle the current number of tasks in
        // queue, but stop if queue becomes empty while doing so.
        int k = Math.min(delta, workQueue.size());
        while (k-- > 0 && addWorker(null, true)) {
            if (workQueue.isEmpty())
                break;
        }
    }
}

修改运行中线程池的核心线程数量需要分两种情况讨论:

  1. 当前线程池运行线程的数量大于要设置的核心线程,则需要调用interruptIdleWorker()方法清楚核心线程的数量并重新分配一些非核心线程到核心线程中
  2. 当要增加核心线程数量的时候,此时当前线程池的运行线程数量是小于核心线程数,因此需要创建核心线程去执行任务,创建的Worker对象的Runnable为空,这样可以更快从任务队列中取出任务执行。如果k=2,任务队列中的任务有10个,则只能增加两个核心线程,如果任务没有执行完,此时只能退出while循环。如果k=10,而任务只有两个,则最多也只需要创建两个线程,因此当workQueue.isEmpty()的时候也会退出循环