java多线程—Executor(线程池)

210 阅读7分钟

线程池的好处

  • 复用线程
  • 控制资源的数量

线程池核心配置

  • corePoolSize 核心线程数
  • maximumPoolSize 最大线程数,当阻塞队列满时,核心线程数会提高
  • keepAliveTime 临时线程的存活
  • handler 拒绝策略
  • threadFactory 线程工厂
  • workQueue 阻塞队列

线程池控制属性

根据juc一贯的套路来说,用一个int的值表示2种状态是比较常见的。比如读写锁ReentrantReadWriteLock 使用AQS中的state来表示,前16位表示共享读锁,后16位表示独占写锁,线程池中使用ctl来表示

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

int 32位 高3位来表示线程池的状态,低29位表示线程数,一共有如下5种状态

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;

同时提供了2个方法来获取各自的值

private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }

线程池执行任务

使用execute方法来提交任务。在这里,代码需要分为3中情况来看

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        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);
    }

execute 执行流程 ,判断当前活跃线程数是否小于当前核心线程数,不小于则申请临时线程数执行任务。若无法申请临时线程,则启动拒绝策略,线程池中的线程是懒加载创建的

情况一: 当前活跃线程数是否小于当前核心线程数,则创建一个新的线程加入线程池

 if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;
    c = ctl.get();
}

addWorker 就是创建新的线程来工作,线程池中会把线程包装成一个worker。command 是加入的任务。

情况二: 当核心线程数已经满时,加入队列,加入后再次检查线程池的运行状态,判断当前线程数是否为0,为0就增加线程。

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

线程加入池子

关键点在于addworker中怎么去创建一个线程 分为如下几个步骤

  • 创建线程
  • 判断是否超过阈值
  • 统计线程数
  • 存放线程数的容器
//2个参数,一个是提交的任务,一个是是否核心线程
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;
    }

这里看到了java中使用了goto这样的语法。第一步修改ctl的值,根据前面讲的3+29,在后面29位中增加线程数,通过cas+自旋的方式修改。可以发现在juc中,只要有cas的操作,一般都结合了自旋,保证线程的安全。

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

在cas修改线程数后,再创建新的worker线程,加锁,放入容器(hashset),启动线程 释放锁 ,finally中处理异常的情况,回滚ctl,hashset中删除wooker,终止线程

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;

加锁的原因是在HashSet是线程非安全的,同时此处还要修改largestPoolSize的值。此处不太理解,因为在CHM的1.7版本和1.8,就是采用了synchronized和cas的方式来提高了性能。此处还在使用ReentrantLock,不知后续是否会变成其他的方式来控制。

关键点在于 HashSet workers 存放wroker 也就是线程的容器

Worker如何工作

在上面看到worker就是线程池中的线程,那么他的创建和工作是怎么样的

// Worker 也是继承了AQS 实现了Runnable
Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;// 设置任务
    this.thread = getThreadFactory().newThread(this); // 从线程工厂中获取一个线程
}

run 方法

public void run() {
    runWorker(this);
}
        // 线程池的方法
        final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // 允许被中断
        boolean completedAbruptly = true;
        try {
        // cas 获取任务直到没有 如果w中传入task就执行,如果没有就获取队列中的任务
            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);
        }
    }

在中w.unlock();到lock之中,代表这个是可以被中断的。在执行前,判断线程池的状态很重要,在停止的状态下,也要中断一下运行的线程。当运行结束后,调用processWorkerExit 退出一个线程,观察一下推出的时候做了什么事情

private void processWorkerExit(Worker w, boolean completedAbruptly) {
        //发送运行异常时,会运行这个分支,减少ctl中的线程数
        if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
            decrementWorkerCount();

        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
        // 统计完成的线程数
            completedTaskCount += w.completedTasks;
            // 移除worker
            workers.remove(w);
        } finally {
            mainLock.unlock();
        }

        tryTerminate();
        // 防止在线程池中没线程,预先加入一个线程
        int c = ctl.get();
        if (runStateLessThan(c, STOP)) {
            if (!completedAbruptly) {
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && ! workQueue.isEmpty())
                    min = 1;
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }
            addWorker(null, false);
        }
    }

到这里为止,从源码中看到了加入和退出线程时发送的事情,但是核心的线程数应该被保留在线程池中,在哪里体现。在一开始获取任务的地方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);

            // 在等待任务时是否允许超时
            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;
            }
        }

了解到队列中poll和take的区别,take会阻塞线程运行,而poll传入时间参数,在运行的时间内等待,超时则跑出异常InterruptedException,此时会推出cas,而核心线程数在运行中,会被take阻塞,不会退出,所以当任务进来时,线程池中有上一次存留下来的线程运行。
如果当前活动线程数大于核心线程数,当去缓存队列中取任务的时候,如果缓存队列中没任务了,则等待keepAliveTime的时长,此时还没任务就返回null,这就意味着runWorker()方法中的while循环会被退出,其对应的线程就要销毁了,也就是线程池中少了一个线程了。因此只要线程池中的线程数大于核心线程数就会这样一个一个地销毁这些多余的线程。

  如果当前活动线程数小于等于核心线程数,同样也是去缓存队列中取任务,但当缓存队列中没任务了,就会进入阻塞状态,直到能取出任务为止,因此这个线程是处于阻塞状态的,并不会因为缓存队列中没有任务了而被销毁。这样就保证了线程池有N个线程是活的,可以随时处理任务,从而达到重复利用的目的。